Arduino Starter Kit

1 Un post sobre Arduino

Des de fa alguns anys que dins del món de programari lliure, o del moviment maker, o des d’entorns educatius i formatius de tecnologia, se sent parlar i es desenvolupa una activitat creixent al voltant d’Arduino (i de Raspberry Pi, Scratch, Processing…)

Em venia de gust provar l’Arduino i això em va dur a comprar, fa uns mesos, un Arduino Starter Kit per a fer-ne els experiments i captar-ne les “sensacions”. Finalment, he trobat temps per completar els experiments i és el moment d’explicar què m’he trobat.

L’Starter Kit deu ser, segurament, la forma més suau d’introduir-se a l’Arduino. És una caixa que inclou una placa Arduino One, una placa protoboard, components electrònics diversos, cables i ponts de connexió i un manual amb quinze experiments -cap d’ells complex, però algun de força vistós- per a realitzar amb el material inclòs.

Clarament l’Arduino Starter Kit està orientat a un públic jove. Les nenes i nens de ESO no haurien de tenir cap dificultat en la realització dels experiments i comprendre la lògica de funcionament dels circuits i els diferents components utilitzats. En cap cas es tracta d’experiments perillosos. Potser els circuits que involucren el motor de continua són els més “complexos” i els que poden presentar major dificultat de muntatge, però els resultats son espectaculars.

Però no us penseu que Arduino és tracta d’una joguina. Les noies i els nois més grans, a nivells de Batxillerat, però també universitaris, poden trobar a l’Arduino una plataforma excel·lent per a la realització de projectes. Fora dels entorns formatius, Arduino ha esdevingut una de les plaques preferides pels aficionats a l’electrònica i pel moviment Maker. Arduino es presenta en diferents versions i existeix una versió industrial per a l’us professional.

2 Però què és Arduino?

(traduccio lliure de https://www.arduino.cc/en/Guide/Introduction)

“Arduino és una plataforma de prototipatge de codi obert basada en hardware i software senzill i fàcil d’usar. Les plaques Arduino són capaces de llegir entrades: com la llum a un sensor, un dit prement un botó, o una piulada de Twitter, i transformar-les en sortides: activant un motor, encenent un LED, o publicant alguna cosa a Internet. Es pot indicar a la placa què ha de fer tot enviant-li instruccions al microcontrolador que porta muntat. Per a aconseguir-ho es fa us del llenguatge de programació de l’Arduino (basat en el llenguatge Wiring), i en l’entorn de desenvolupament integrat d’Arduino (Integrated Development Enviroment, o IDE), basat en l’entorn Processing.

Amb el pas dels anys Arduino ha estat el cervell de milers de projectes, desde objectes quotidians fins a instruments científics complexos. Una comunitat mundial de ‘makers’ -estudiants, aficionats, artistes, programadors i professionals- s’ha aplegat al voltant d’aquesta plataforma de codi obert. Les seves contribucions hab afegit una quantitat increible de coneixement accessible que pot ser de gran ajuda tant per als principiants com per als més experts.

Arduino va neixer al Ivrea Interaction Design Institute com una eina d’utilització fàcil per al prototipatge ràpid adreçada a estudiants sense fonaments en electrònica i programació. Tan aviat com va arribar a una comunitat més gran, la placa Arduino va començar a canviar per adaptar-se a noves necessitats i desafiaments, diversificant la seva oferta desde plaques simples de 8 bits, fins productes per a aplicacions de Internet of Things (IoT), wearables, impressió 3D i sistemes encastats. Totes les plaques Arduino són completament de codi obert, permetent als usuaris construir-les pels seus propis mitjans i adaptar-les als seus requeriments particulars. El programari també és de codi obert i evoluciona amb les constribucions dels usuaris d’arreu del món.

Mercès a la seva accessibilitat i simplicitat d’us, Arduino s’utilitza en milers de projectes i aplicacions diferents. El programari d’Arduino és d’us senzill per als principiants però, a l’hora, prou flexible per usuaris experimentats. Es pot executar en Mac, Windows i Linux. Els professors i els estudiants el fan servir per construir instruments científics barats, o per provar principis de física i química, o per a iniciar-se en la programació i la robòtica. Els dissenyadors i els arquitectes construeixen prototipus interactius. Músics i artistes el fan servir en instal·lacions i per experimentar amb instruments musicals nous. Els ‘Makers’, per descomptat, el fan servir per construir molts dels projectes que s’exhibeixen a les ‘Maker Faire’, per exemple. Arduino és una eina clau per a l’aprenentatge de novetats. Tothom – nens, aficionats, artistes, programadors – es pot posar mans a l’obra ja sigui seguint les instruccions pas a pas d’un kit, o compartint idees amb altres membres de la comunitat Arduino.

Hi han molts altres microcontroladors i plataformes de microcontrolador per a la construcció de sistemes físics interactius (“physical computing”). Parallax Basic Stamp, BX-24 de Netmedia, Phidgets, Handyboard del MIT, i molts altres que ofereixen funcionalitats similars. Totes aquestes eines encapsulen els detalls més farragosos de la programació de microcontroladors i el empaqueten en una interficie d’usuari més senzilla d’utilitzar. Arduino també simplifica el procés de treball amb microcontroladors i, a més, ofereix alguns avantatges per als professors, estudiants, i aficionats que no tenen els altres sistemes:

  • Barat – Les plaques Arduino són relativament barates si es comparen amb altres plataformes de microcontroladors. La versió més barata d’Arduino es pot muntar a mà, i els mòduls de version ja muntades, no arriben als 50 dòlars.
  • Multiplataforma – L’entorn de programació d’Arduino s’executa a Windows, Macintosh OSX, i distribucions Linux. La majoria de plataformes de microcontroladors estan limitades a Windows.
  • Entorn de programació senzill – L’entorn de programació és fàcil d’usar pels principiants, però prou flexible com per a que els usuaris avançats el puguin fer servir eficaçment. Per als mestres, l’entorn integrat està basat en l’entorn de programació ‘Processing’, per tant, els estudiants que estiguin aprenent a programar en aquest es trobaran que els serà familiar la forma de treballar de l’IDE d’Arduino (veure nota).
  • Codi obert i programari extensible – El programari Arduino es codi obert i es pot extendre. El llenguatge de programació es pot ampliar mitjançant llibreries C++, i la gent aque vulgui comprendre els detalls més tècnics poden fer els salt des del llenguatge de l’Arduino IDE al llenguatge de programació C per AVR en que està basat. També es pot afegir codi AVR-C directament als programes d’Arduino si es vol.
  • Codi obert i maquinari extensible – Els esquemes de les plaques Arduino es publiquen amb llicència Creative Commons, per tant els dissenyadors mes experimentats poden fer les seves pròies versions dels mòduls, extendre-les o millorar-les. Fins i tot, un principiant pot construir la seva pròpia versió dels mòduls en una placa protoboard, per a comprendre’n el funcionament i per estalviar diners.”

Nota: realment l’IDE de l’Arduino està basat en el IDE Wiring (basat en el llenguatge C), que al seu temps ho està en el IDE de Processing (basat en llenguatge Java)

3 Especificacions d’Arduino One

Les podeu trobar a la pàgina oficial. Les reprodueixo a continuació:

Technical specs
Microcontroller ATmega328P
Operating Voltage 5V
Input Voltage (recommended) 7-12V
Input Voltage (limit) 6-20V
Digital I/O Pins 14 (of which 6 provide PWM output)
PWM Digital I/O Pins 6
Analog Input Pins1 6
DC Current per I/O Pin 20 mA
DC Current for 3.3V Pin 50 mA
Flash Memory 32 KB (ATmega328P) of which 0.5 KB used by bootloader
SRAM 2 KB (ATmega328P)
EEPROM 1 KB (ATmega328P)
Clock Speed 16 MHz
Length 68.6 mm
Width 53.4 mm
Weight 25 g

I també l’esquemàtic.

4 Els circuits.

L’Starter Kit proposa la construcció de quinze circuits molt senzills. Per a cada circuit es proporciona:

  • la llista de components a utilitzar
  • l’esquema
  • la foto del circuit muntat
  • la descripció pas a pas del muntatge dels components
  • el codi C comentat que cal compilar i carregar a l’Arduino
  • com provar el circuit
  • propostes d’ampliació i variacions

4.1 Experiment 1.

S’expliquen els contactes de la protoboard; S’enuncien els rudiments dels circuits elèctrics i la llei d’Ohm, i es fan circuits mínims en serie i paral·lel. L’Arduino només es fa servir per proporcionar l’alimentació dels circuits.

4.2 Experiment 2.

És un experiment mínim que permet introduir l’Arduino IDE. Es programa una seqüència d’encesa i apagat de tres leds fent servir sortides digitals. S’aprèn a compilar i carregar els programes a la placa. Ens planteja l’estructura bàsica dels programes Arduino, amb la funció setup() que s’executa al començament de l’execució i que s’aprofita per a inicialitzar l’estat de la placa; i la funció loop() que és una funció que s’executa contínuament, i que és on es programa la funcionalitat desitjada. Les funcions setup() i loop() apareixen sempre en tots els programes d’Arduino.

S’introdueix una tècnica per reconèixer els canvis d’estat als ports d’entrada de l’Arduino.

4.3 Experiment 3.

S’introdueix el sensor de temperatura TMP36 que es capaç de proporcionar un rang de tensions de sortida que varia proporcionalment a la temperatura en graus centígrads. Es mostra l’ús de les entrades analògiques i una tècnica simple d’escalat de valors. S’introdueix el Monitor Sèrie, que permet obtenir informació i missatges provinents de la placa Arduino, i també per enviar senyals a la placa. Es fan servir les sortides digitals per simular un termòmetre fet amb leds.

4.3.1 Comunicació entre Arduino i Host pel port sèrie (USB) amb altres llenguatges

La comunicació sèrie entre l’ordinador (el host) i la placa Arduino mitjançant el port sèrie (USB) es pot fer amb varietat de llenguatges, no només amb l’Arduino IDE.

En particular, és molt senzill fer la comunicació amb aplicacions desenvolupades amb C/C++ perquè es fa servir la llibreria C estàndard. És dir, amb C/C++ n’hi ha prou amb obrir el port /dev/ttyACM0 amb fopen per a lectura/escriptura i fer servir la resta de funcions de I/O de la la llibreria estàndard: fread, fwrite, fclose…

Alternativament, també es poden fer servir les crides de baix nivell: open, write, read, close… la tria d’un o altre grup de funcions, doncs, dependrà d’altres criteris, com pot ser el grau d’abstracció que es requereixi en la comunicació entre host i Arduino. En general, però, sempre caldrà tenir en compte mantenir la consistència amb el mètode triat (Llibreria Estàndard de C, o Crides al Sistema),

Amb Python, la comunicació entre Arduino i Host es pot fer fàcilment amb la llibreria pySerial.

La llibreria s’ha de carregar. En el meu cas que faig servir una distribució Lubuntu 15.04 he de fer:

sudo apt-get install python-serial

El següent és un exemple bàsic que permet llegir els missatges que venen de la placa Arduino:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import serial
 
ser = serial.Serial('/dev/ttyACM0', 9600)
while True:
   print ser.readline()

Amb Java, es pot fer servir la llibreria RxTx. Es tracta d’un jar amb classes java i d’una so (per Linux, Mac) o dll (per Windows) que cal instal·lar. Val a dir que aquesta llibreria va inclosa amb l’entorn Processing, així que si teniu instal·lat aquest entorn ja teniu la RxTx al vostre sistema.

4.4 Experiment 4.

En aquest experiment es fan servir tres fotoresistències per a modular els senyals dels components blau, ved i vermell d’un LED RGB de càtode comú: en funció del grau d’il·luminació de cadascuna de les fotoresistències s’aconsegueix que la llum del LED variï entre el blanc (tots els sensors totalment il·luminats), tons i barreges de colors (diferents intensitats de llum sobre cada sensor), o l’apagat (amb els sensors de llum tapats).

Per a simular les sortides de diferents intensitats amb les sortides digitals s’aprofita que les sortides 3,5,6,10 i 11 de l’Arduino admeten la modulació PWM, (és dir, regular el cicle de treball del pols digital i, per tant, modular-ne el valor del component de CC). En la programació del codi es té en compte el temps necessari per a la conversió AD a les entrades connectades als sensors.

4.5 Experiment 5.

El servomotor és l’estrella de l’experiment 5. L’alimentació de 5V de l’arduino és suficient per alimentar el servomotor. Al tractar-se d’una càrrega inductiva cal protegir la placa dels corrents espúris produits pel servo: en aquest circuit s’afegeixen un parell de condensadors de desacoblament,

Al codi cal destacar

  • L’utilització de la llibreria Servo, que encapsula la utilització de sevomotors de Parallax.
  • L’us de la funció map() (veure nota) que permet fer un mapeig escalat lineal entre un rang d’entrada i un de sortida.

Per a saber més de motors i servos, se’ns recomana el llibre Making Things Move de Dustyn Roberts i l’adreça http://robives.com/mechs

nota: aquesta funcio map() no té res a veure amb la funció homònima que es pot trobar als llenguatges que segueixen el paradigma de programació funcional!

4.6 Experiment 6.

Del moviment al sò. Conceptualment és similar a l’experiment 4. Es fa servir una fotoresistència per modular el senyal que ataca un brunzidor piezoelèctric. El senyal del sensor es converteix AD a un valor que es fa correspondre amb map() (veure nota) a un valor dins el rang de control de la sortida PWM que ataca al brunzidor.

La novetat en aquest circuit és que en comptes de fer servir la funció analogWrite() es fa servir la més adequada tone(): Amb analogWrite() la freqüència que s’envia per la sortida és fixa però amb un cicle de treball (relació entre temps en estat alt i període del senyal) variable; amb tone() el cicle de treball és sempre del 50%, però canvia la freqüència del senyal.

Al codi també s’introdueix la funció millis() per controlar un temporitzador.

4.7 experiment 7.

Un altre experiment amb el brunzidor. En aquesta ocasió se substitueix el sensor de llum per una escala de resistències combinada amb uns interruptors, de forma que a la pulsació de cada interruptor li correspon un valor de tensió en l’entrada AD de l’Arduino. A cada valor d’entrada se li fa correspondre una freqüència, amb la que s’ataca al brunzidor.

la novetat al codi és la utilització d’un array de C per mantenir la taula de freqüències, i la utilització de noTone() per silenciar el brunzidor quan no hi ha cap interruptor premut.

Una taula amb les freqüències de les notes musicals es pot consultar a http://arduino.org/frequencies.

4.8 Experiment 8.

En aquest experiment es fa servir un interruptor d’inclinació per a simular un rellotge de sorra: al moure l’interruptor que està connectat a una entrada digital que s llegeix amb digitalRead(), es restaura l’estat de les sortides digitals que estan connectades a uns leds. Aleshores, a la funció loop(), en cada iteració s’observa l’hora actual que s’obté amb millis() i es compara amb l’hora en que s’ha encès el darrer LED. Si l’interval de temps supera un valor prefixat, s’encén el següent LED i s’actualitza la marca.

4.9 Experiment 9.

Es presenta el motor de continua. El motor de CC és de velocitat i sentit de gir variables. la velocitat de gir es pot controlar variant la tensió aplicada a les seves connexions (dins d’uns límits), i el sentit de gir es pot canviar invertint-ne la polaritat.

Tanmateix, fins i tot amb el petit motor inclòs a l’Starter Kit, l’Arduino no és capaç de proporcionar prou corrent per a alimentar motors directament: Arduino One no pot proporcionar més de 40mA per les sortides digitals.

El que es fa al circuit és connectar el motor a una font externa i controlar-ne l’alimentació fent servir un transistor MOSFET IRF520 com a interruptor electrònic, connectant-ne la porta a una de les sortides PWM i fent el control efectiu de l’encesa i apagada amb un interruptor connectat a una de les entrades digitals.

Al manual del kit proposen fer servir una pila de 9V per a la font externa. També es pot fer servir una font d’alimentació de les que es poden trobar a qualsevol basar xinès.

Addicionalment, es fa servir un díode rectificador N4007 connectat en oposició a l’alimentació del motor, per a proporcionar un camí de descàrrega per als corrents auto-induits quan aquest es desconnecta de l’alimentació i es va frenant fins aturar-se.

4.10 Experiment 10.

A l’experiment anterior es presentava el motor de CC i es muntava un circuit molt senzill per a engegar-lo i aturar-lo, però s’indicava que és possible regular-ne la velocitat i l sentit de gir. En aquest experiment es fa servir un circuit integrat L293D de driver per a motors per aconseguir aquest control.

L’ús de l’L293D simplifica el control dels motors: un xip substitueix el circuit del MOSFET i al díode de protecció. Però, a més, és possible realitzar el control de velocitat i la inversió de gir (un pont H) amb aquest CI.

És una bona idea revisar l’especificació de l’L293D per revisar diferents modalitats de connexió, o la connexió de fins a quatre motors amb aquest driver.

4.11 Experiment 11.

Es presenta el dispositiu LCD de visualització LCM1602C, es tracta d’un pantalla LCD de 16×2 caràcters. El muntatge connecta les sortides digitals de l’Arduino al display LCD per a mostrar missatges que canvien aleatòriament quan es mou l’interruptor d’inclinació connectat a una de les entrades digitals. És un projecte similar al de l’experiment 8, només que en comptes d’enviar els missatges aleatoris pel monitor sèrie, els mostra a l’LCD.

La lògica de comunicació entre Arduino i l’LCD s’encapsula a la llibreria LiquidCrystal. La llibreria s’explica amb detall a la url: http://arduino.org/lcd.

Pel que fa al muntatge, val a dir que hi ha alguna etiqueta de l’esquema al manual que no coincideix exactament amb l’etiqueta impresa al component LCD i potser pot produir alguna confusió. En tot cas, una revisió de les especificacions de l’LCM1602C haurien de ser suficients per esvair qualsevol dubte en el cablatge.

4.12 Experiment 12.

Un altre experiment amb el servomotor. En aquesta ocasió es fa servir el brunzidor piezoelèctric com a sensor, en lloc d’actuador. El brunzidor, a més de produir so, també el pot captar i, per tant, en aquesta ocasió es connecta a una entrada analògica de l’Arduino, enlloc de a una sortida.

El circuit de l’experiment fa un comptador de tocs (per exemple, el soroll de picar de mans).

El programa és un exemple de com implementar una senzilla màquina d’estats. El funcionament implementat va fent transicions des de l’estat de repòs (leds taronja i verd apagats, led vermell encès, servo en posició de “tancat”), a l’estat d’un toc vàlid captat (leds verd i vermell apagats, led taronja encès, servo “tancat”), dos tocs vàlids captats (mateixes condicions), i tres tocs vàlids captats (leds vermell i taronja apagats, led verd obert, i servo en posició d'”obert”), on roman fins que es rep un reset que torna a l’estat de repòs original.

4.13 Experiment 13.

l’Starter Kit ens proposa la construcció d’un “component” capacitiu amb paper d’alumini que es pugui fer servir amb la llibreria CapacitiveSensor de Paul Badger.

El component capacitiu no és més que una làmina de paper d’alumini.

El principi de funcionament d’aquest “sensor” és la càrrega i descàrrega de les capacitats paràsites que apareixen al circuit. Per a major detall, podeu consultar http://playground.arduino.cc/Main/CapacitiveSensor

També és interessant que, a diferència d’altres llibreries usades en els experiments, cal descarregar la CapacitiveSensor d’Internet i instal·lar-la correctament a l’Arduino IDE per a poder fer-la servir.

4.14 Experiment 14.

És possible “parlar” amb l’Arduino amb varietat de llenguatges, entorns i aplicacions, com podeu comprovar a http://playground.arduino.cc/Main/Interfacing.

A l’experiment 14 s’envia un senyal variable des de l’arduino pel monitor sèrie (pel port USB). El senyal és genera amb un divisor de tensió fet amb un potenciòmetre connectat a una entrada analògica, però en aquesta ocasió no es manipula cap sortida si no que el valor capturat s’envia pel port sèrie.

La novetat de l’experiment és que en comptes de capturar la dada enviada per la placa amb el Monitor Sèrie de l’Arduino IDE es fa servir una petita aplicació desenvolupada amb Processing. De la wiki: “Processing és una aplicació de codi obert amb un llenguatge per a la programació d’imatges, animació, i so. El processing és un projecte de codi lliure iniciat per Ben Fry (Institut Ample) i Casey Reas (UCLA Design / Media Arts), i és desenvolupat per artistes i dissenyadors com a alternativa a eines de programari patentades en el mateix camp, com Macromedia Flash o Director.”

Processing i Arduino IDE són molt similars. El paral·lelisme va més enllà del look-and-feel de l’entorn: els programes amb l’Arduino IDE són, bàsicament, un setup() i un loop(); amb Processing tenim també un setup() que s’executa un cop a l’inici de l’execució; i un draw(), que al igual que loop() s’executa contínuament.

Processing compta amb moltes llibreries realitzades i depurades per usuaris i compartides amb la comunitat, igual que l’Arduino IDE. En particular, compta amb una llibreria procesing.serial basada en la RxTx per a realitzar comunicacions pel port USB (serie). Internament, RxTx es basa en una llibreria nadiua al sistema operatiu hoste, que s’encapsula amb el mecanisme JNI de Java, i unes classes java que exposen la interfície de programació.

El programa proposat amb Processing escolta el port serie i, segons les dades que va rebent (entre 0 i 255, que corresponen als nivells de tensió variables amb el potenciòmetres connectat a l’entrada analògica de l’Arduino) es va canviant el color de fons d’una pantalla que mostra una imatge del logo d’Arduino.

4.14.1 Experiment 14 Amb Python, Pygame i pySerial

El cas és que m’he trobat un parell de problemes al codi del manual: el primer és que la imatge que es fa servir és referencia a través d’una URL. Aquesta URL ja no està disponible i cal canviar-la per una altre que que sí es pugui accedir. I segon: em fa l’efecte que el codi que es fa servir no està actualitzat a la darrera versió de la llibreria.

Vistes aquestes “dificultats” he optat per buscar una solució alternativa. Processing està molt bé, però com Python m’agrada molt, he assajat una solució amb Python i les llibreries Pygame i pySerial. Vet aquí el codi:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pygame, sys, serial
from pygame.locals import *

if __name__ == "__main__":
    pygame.init()
    FPS = 25 # frames per second setting
    fpsClock = pygame.time.Clock()
    
    # set up the window
    board = pygame.display.set_mode((400, 300), 0, 32)
    pygame.display.set_caption('Arduino')

    # set up serial port
    ser = serial.Serial("/dev/ttyACM0", baudrate=9600)
    
    while True: # the main loop
        if ser.isOpen():
            A0_385 = ser.read(385) # 385 = 9600 / FPS
            A0 = ord(A0_385[0])
            FILL_COLOR = (A0, A0, A0)
        
            board.fill(FILL_COLOR)
        
        for event in pygame.event.get():
            if event.type == QUIT:
                ser.close()
                pygame.quit()
                sys.exit()

        pygame.display.update()
        fpsClock.tick(FPS)

Els programes fets amb Pygame tenen aquesta mateixa estructura setup-loop que es pot trobar als programes de Processing, o als programes de l’Arduino. És fàcil, doncs, “anar” de l’un a l’altre.

Val a dir que m’he trobat amb una dificultat que no havia previst: les diferents velocitats entre la producció de dades de la placa i la velocitat de consum, donada pel nombre d’iteracions per segon del bucle de lectura de dades i d’actualització de pantalla, que està controlat per fpsClock.tick(FPS).

La solució ha estat “descartar frames” que és el que aconsegueixo amb:

A0_385 = ser.read(385) # 385 = 9600 / FPS
A0 = ord(A0_385[0])
FILL_COLOR = (A0, A0, A0)

És dir llegeix 385 caràcters el buffer (385 = bauds / fps), però només té en compte el primer. Més que una dada precisa que permeti buidar el buffer de lectura en cada iteració, 385 (o 400) és, més aviat, un ordre de magnitud que es podria ajustar.

En tot cas, aquest valor funciona bastant bé, i movent el potenciòmetre de la placa, aconsegueixo que el color de fons de la pantalla variï gradualment en una escala de grisos entre el blanc i el negre.

4.14.2 Una “variació” de l’experiment 14 amb Scratch for Arduino

Scratch for Arduino (S4A) és un desenvolupament realitzat al Citilab de Cornellà.

De la pàgina web http://s4a.cat/index_ca.html: “Què és S4A? S4A és una modificació d’Scratch que permet programar la plataforma de hardware lliure Arduino d’una manera senzilla. Proporciona blocs nous per tractar amb sensors i actuadors connectats a una placa Arduino. També compta amb un panell de sensors similar al de la PicoBoard.

La finalitat principal del projecte és atreure gent al món de la programació. Un altre objectiu és proporcionar una interfície d’alt nivell per a programadors d’Arduino amb funcionalitats tals com la interacció amb un conjunt de plaques mitjançant esdeveniments d’usuari.”

Som-hi. Realment, aquest versió de l’experiment no segueix ben bé la mateixa lògica perquè amb S4A no puc llegir directament el monitor sèrie. El que faig es llegir el senyal de l’entrada analògica amb un sensor d’Scratch, i faig servir aquest valor per moure un sprite amb el gat d’Scratch per la pantalla.

El procés és el següent.

Descarrego i instal·lo l’S4A i el firmware de connexió (el programa d’interfície amb Scratch que realment s’executa a la placa Arduino i que és el que controla el port serie).

El circuit a la placa Arduino és el de l’experiment 14 tal com apareix al manual: un potenciòmetre fent de divisor de tensió entre Vcc i GND amb la pota de control connectada a l’entrada analògica A0.

Connecto per USB l’ordinador i l’Arduino i fent servir l’Arduino IDE carrego el firmware a la placa.

Ara engego l’S4A. No ha d’haver cap problema. L’S4A reconeix una placa Arduino connectada.

Aleshores, amb l’Scratch preparo una nova variable v_posx:

1-variable

aquest bloc per a “l’escenari”:

2-escenari

aquest per al gat:

3-gat

Li dono a la bandereta verda i, efectivament, ara tinc un gat corrent per la pantalla controlat pel potenciòmetre del circuit a la placa Arduino. Fixeu-vos en el valor de la variable de l’entrada A0 a les imatges.

4.15 Experiment 15.

I per acabar la revisió de l’Starter Kit, a l’experiment 15 se’ns anima a buscar aquelles andròmines velles que siguin susceptibles d’algun tipus de control electrònic i tractar d’imaginar com es podrien connectar a l’Arduino, i s’aprofita per presentar l’opto-acoblador 4N35.

Títols de crèdit com els d’Star Wars (efecte “rolling”) amb Python

1 Fa molt temps (però no en una galàxia molt llunyana)…

… vaig a anar al cine -i era un dels primers cops que hi anava- per veure “La guerra de las galaxias”, que era com es va traduir el nom de “Star Wars: A new hope”.
La pel·lícula em va impressionar des del primer segon, començant pels imponents títols de crèdit que introduïen la història i que s’allunyaven majestuosament per l’espai.

Per a la realització de l’efecte dels títols allunyant-se es van utilitzar tècniques “artesanals”. En aquest enllaç expliquen com, però una foto val més que mil paraules:
Star-Wars-Intro-Creation-Secret-2
Des de l’arribada de la informàtica personal s’han inventat de forma recurrent sistemes per reproduir l’efecte amb ordinadors. Una cerca a Google ens mostra un munt de resultats.
L’efecte es pot aconseguir directament amb diverses aplicacions, però segueix sent un repte interessant la seva realització per programa. En el post d’avui, doncs, presento un “prova de concepte” feta amb Python per a generar aquest efecte de rolling a uns títols de crèdit.

2 Com es fa una animació?

Es sabut que una animació no és més que la successió ràpida d’imatges (frames, o fotogrames). Per tant, per a fer una animació només cal mostrar una imatge durant un breu instant, a continuació una altre amb un petit canvi, després un altre… la integració de la successió de les imatges estàtiques al nostre cervell és el que provoca la sensació de moviment. Aleshores, per a fer una animació el que he de fer és crear els diferents fotogrames que la formen. La sensació de moviment suau depèn de quantes imatges (o frames) per segon es fan servir: amb 12 frames per segon (fps) s’arriben a notar alguns salts; a 25fps la sensació de moviment és pràcticament perfecte, i és el valor que es fa servir a les càmeres de vídeo.

2.1 Un experiment

Fem un petit experiment. Agafem aquesta imatge que és de 606×700
swtfa

En comptes de visualitzar-la sencera…

  • en visualitzaré només un quadre de 320×240;
  • aniré desplaçant el quadre un píxel per cop cap avall;
  • a una velocitat de 10 píxels per segon, és dir, a 10fps.

Faig servir la funció blit de la llibreria pygame per mostrar només una part de la imatge.

blit-image.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pygame as pg
from pygame.locals import *

pg.init()                           # init pygame
w = 320                             # width
h = 240                             # height
size=(w,h)                          # size
screen = pg.display.set_mode(size)  # init screen
pg.display.set_caption('Star Wars TFA')  # caption
filename = "./swtfa.jpg"            # filename
img=pg.image.load(filename)         # image 606x700
x = 0
y = 0
FPS = 10                            # frames per second setting
framerate = pg.time.Clock()
repeat = True
   
while repeat:                       # iterate
    # put image
    screen.blit(img, (0,0), (x, y, 320, 240)) 
    pg.display.flip() 
          
    # scroll down image
    y = y + 1
    if (y + 240 > 800):
      y = 0

    #10fps
    framerate.tick(FPS)

    # capture events
    for event in pg.event.get():
      if event.type == QUIT:
          repeat = False

# exit          
pg.quit()    
print "done!"

Si executo el programa anterior sembla que la imatge dins el requadre es vagi movent cap avall. He aconseguit crear la sensació de moviment a partir d’una imatge estàtica. Amb els títols de crèdit he de fer el mateix: construiré una imatge amb el text i aniré obtenint-ne els diferents frames sense més que anar desplaçant-me un píxel cap avall per a cada frame. Aleshores, un cop tingui tots els frames base, aplicaré una transformació sobre cada frame per afegir l’efecte d’allunyament.

3 Esquema general

La idea és partir d’un text d’entrada inicial i obtenir, com a resultat final, un vídeo amb l’scrolling del text.
Especifico més: el vídeo serà de format mp4 de 320×240 px.

El procés es pot dividir en sub-processos més senzills. Em plantejo aquests quatre passos:

  • donar al text un format adequat per a ser processat.
  • crear els frames base del vídeo (imatges o frames de 320×240)
  • processar els frames per afegir l’efecte d’allunyament: el resultat de processar cada frame serà un nou frame, també de 320×240.
  • crear un vídeo amb els frames processats.

Som hi.

4 Donar al text un format adequat

Primer de tot, el text a mostrar:

EPISODI V
L'IMPERI CONTRAATACA
Les forces imperials avancen implacablement 
de victòria en victòria sobre els rebels. 
L'Imperi prepara el cop definitiu: 
ha descobert la principal base rebel 
al planeta gelat de Hoth 
i es llença a l'atac per destruir-la.
Però no tot està perdut per 
als rebels. 
Si aconsegueixen retenir prou temps 
a les tropes d'assalt, la flota rebel 
podrà escapar a la nova base secreta...

Ara he de preparar aquest text per a que em sigui fàcil generar els frames de la imatge i processar-los.

La idea és aquesta: vaig a convertir el text en una imatge de 320px d’ample (ample de imatge que coincideixi amb l’ample de frame) per el llarg suficient per encabir-hi el text, més 240px (un frame) addicionals en blanc al principi i 240px (un frame addicional) en blanc al final.

Amb aquestes restriccions (i després d’algunes proves) resulta que la mida 320×800 em serveix.

I quin format? La restricció és fer-m’ho fàcil i que tot sigui molt evident i clar. El format més adequat que he trobat és el pbm:

https://en.wikipedia.org/wiki/Netpbm_format

Es tracta d’un format monocrom molt senzill. Reviso l’exemple que apareix a la wiki:

P1
# This is an example bitmap of the letter "J"
6 10
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
1 0 0 0 1 0
0 1 1 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0
  • ‘P1’ indica que és el format pbm, es dir monocrom amb dades en format ascii
  • ‘# This is…’ és un comentari
  • ‘6 10’ diu que és una imatge de 6×10 píxels
  • ‘0’ indica punt en blanc ‘1’ punt en negre
  • A més, els espais en blanc, i salts de línia es descarten

Tenint en compte tot l’anterior, genero amb GIMP un llenç de 320×800 de fons negre; hi afegeixo el text deixant 240px abans de l’inici i 240px després.

El resultat és aquest:

intro

5 crear el fitxer “master”

Si examino intro.pbm veig el següent:

albert@eowyn:~/workspace/python/starwars-credits$ head intro.pbm
P1
# CREATOR: GIMP PNM Filter Version 1.1
320 800
1111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111
albert@eowyn:~/workspace/python/starwars-credits$ wc intro.pbm
  3660   3668 259707 intro.pbm
albert@eowyn:~/workspace/python/starwars-credits$

La part de la imatge es divideix en línies de 70 caràcters. L’especificació dels formats pbm, pgm, ppm recomana que les línies de dades no superin els 70 caràcters, però només és una recomanació. Aprofito aquest grau de llibertat perquè em sembla que és més senzill transformar el bloc de 70×3657 (3660 línies menys les tres de capçalera) en una bloc de 320(+ 1 de retorn de carro)x800 per a poder moure’m directament als inicis de cada fila d’imatge i agafar blocs de 240 files per a generar els frames. O sigui, em preparo un “master” per a poder generar més senzillament els frames. Ho faig amb el següent codi:

w = 320                                   # width of a frame and base image
h_frame = 240                             # height of a frame
h_img_base = 800                          # height of base image
c = 0                                     # counter
frame_size=(w,h_frame)                    # size of frame
filename_read = "intro.pbm"
frames_folder = "./frames/"
filename_master = "master"
bit = ""

# create master
with open(filename_read, "r") as fr:    
    with open(frames_folder + filename_master, "w") as fm:
        # discards three first header lines
        fr.readline()
        fr.readline()
        fr.readline()

        for i in xrange(0, w * h_img_base):
            c = c + 1
            bit = fr.read(1)
            if bit == '\n':
                bit = fr.read(1)
            fm.write(bit)

            if c == w:
                fm.write('\n')
                c = 0

El resultat de l’anterior és un fitxer master de 800 files de 320 caràcters (més un caràcter de retorn de carro a cada línia).

6 creació dels frames

A partir del master és molt senzill generar els frames. Això ho faig amb el següent codi

# create frames
filename_frame = ""
filename_pattern = "frame_%0#5d.pbm"
frame_count = 0

with open(frames_folder + filename_master, "r") as fr:
    for frame_count in xrange(0, h_img_base - h_frame): 
        filename_frame = filename_pattern % frame_count
        print "[Frame %d]" % frame_count

        with open(frames_folder + filename_frame, "w") as fw:
            fw.write("P1\n")
            fw.write("# CREATOR: Albert Baranguer Codina\n")
            fw.write("%d %d\n" % frame_size)

            fr.seek(frame_count * (w + 1))

            for i in xrange(h_frame ):
                fw.write(fr.readline())

Remarcar que amb

fr.seek(frame_count * (w + 1))

Em situo a l’inici de cada frame, i amb

for i in xrange(h_frame ):
    fw.write(fr.readline())

llegeixo el bloc de 240 línies. En aquest moment, després d’executar el bloc anterior, tinc 560 fitxers pbm (560 = 800 – 240) amb noms de frame_00000.pbm a frame_00559.pbm a la carpeta frames. Cada fitxer té un bloc d’imatge de 240 línies de 320 caràcters, més un de retorn de carro, per fila.

7 EL vídeo sense processar

Puc fer servir els frames generats per visualitzar la versió del vídeo sense l’efecte d’allunyament. Faig servir el següent visualitzador:

player1.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pygame as pg
from pygame.locals import *

pg.init()                           # init pygame
w = 320                             # width
h = 240                             # height
size=(w,h)                          # size
screen = pg.display.set_mode(size)  # init screen  
FPS = 18                            # frames per second setting
framerate = pg.time.Clock()
pg.display.set_caption('STAR WARS CAPTION FX')

filename_processed_pattern = "frame_%0#5d.pbm"
processed_frames_folder = "./frames/"  

filename_frame = ""
repeat = True
while repeat: # iterate
    for frame_count in range(0, 560):
        filename_frame = filename_processed_pattern % frame_count
 
        img=pg.image.load(processed_frames_folder + filename_frame)
        screen.blit(img, (0,0)) # put image 
        pg.display.update()
        pg.display.flip() 
        framerate.tick(FPS)

        for event in pg.event.get():
            if event.type == QUIT:
                repeat = False

        if not repeat:
            break

pg.quit()    
print "done!"

8 Filtre d’allunyament

Un cop tinc tots els frames, aplico a cadascun el filtre “d’allunyament”. La idea és mapejar  el frame rectangular a  un trapezi:

(0,0)--------------------------------------(319,0)
  |                                           |
  |                                           |
  |     (c, e)----------------------(d, e)    |
  |       |                           |       |
  |      |                             |      |
  |     |                               |     |
  |    |                                 |    |
  |   |                                   |   |
  |  |                                     |  |
  | |                                       | |
  ||                                         ||
  |                                           |
(239,0)----------------------------------(319,239)

és dir, vull una funció que transformi els punts del rectangle del frame en punts del trapezi definit per (c,e) (d,e) i els cantons inferiors del frame

  • (0,0) –> (c, e)
  • (319, 0) –> (d, e)
  • (239, 0) –> (239, 0)
  • (319, 239) –> (319, 239)

Després d’algunes proves, trio els valors e = 40, d = 199 i, per simetria, c = 319 – d = 120.

Horitzontalment, la transformació serà un escalat lineal depenent de l’alçada dins del trapezi.

A l’eix vertical la densitat del mapeig ha d’anar creixent a mida que pugem pel trapezi. És a dir, a mida que ens apropem al costat superior del trapezi han d’haver-hi cada cop “més línies”.

Combinant aquestes dues transformacions s’aconsegueix fer les lletres cada cop més petites a mida que “van pujant”.

Donat un punt del frame (x0, y0) caldrà, doncs, determinar a quina posició vertical es desplaça yt i, amb aquesta dada, determinar quin escalat horitzontal li correspon yt.

És dir, una mapeig del punt (x0, y0) al punt (xt, yt)

T:(x0, y0)——> (xt, yt)

Aquesta transformació la resolc amb la següent funció

def transform(x0, y0):
     a = 319.0
     b = 239.0
     d = 199.0
     e = 40.0
     c = a - d

     # lineal
     # y1 = e + (y0 * ((b - e) / b))
     
     # parabolic
     c0 = (b - e) / (b * b)
     c1 = e
     y1 = (c0 * y0 * y0) + c1


     x2 = (((y1 * c) - (b * c)) / (e - b))
     x3 = a - x2
     x1 = x2 + (((x3 - x2) / a) *  x0)

     return (x1, y1)

El mapeig en vertical es fa amb

# lineal
# y1 = e + (y0 * ((b - e) / b))

# parabolic
c0 = (b - e) / (b * b)
c1 = e
y1 = (c0 * y0 * y0) + c1

Al codi està comentada la línia que caldria per a fer un mapeig lineal. És dir, amb “densitat” de línies homogènia.

Si proveu el mapeig vertical lineal veureu que les lletres mantenen l’alçada durant tot el seu recorregut per la pantalla.

El que aplico és el mapeig “parabòlic”, en comptes de lineal.

La següent línia defineix una paràbola amb el vèrtex (0, c1), on c1 és 40.

y1 = (c0 * y0 * y0) + c1

El valor de c0 es determina amb la següent línia

c0 = (b - e) / (b * b)

Amb aquesta definició de c0, quan y0 és b (que té el valors de 239, és dir, última línia) y1 val b, com es pot comprovar fàcilment.

C0 és molt petit (està dividit pel quadrat de 239), de forma que amb valors “petits” de y0 tinc valors petits de y1.

El resultat és que n línies y0 es mapegen a la mateixa línia y1 (n > 1)

Però el creixement parabòlic fa que a mida que creix y0, en el mapeig d’n a 1 la n  cada cop es fa més petita.

De fet, a mida que y0 s’apropa al valor de 239 (la línia inferior) la n passa a ser fraccionària. Caldrà tenir-ho en compte mes tard.

He fet servir una funció “parabòlica” per a augmentar la densitat de línies prop de la línia superior, però és podria provar una altre funció que donés un resultat similar, a veure quin efecte té.

Un cop tinc la y1, aleshores puc calcular l’escalat horitzontal. En aquest cas es tracta d’una escalat lineal.

x2 = (((y1 * c) - (b * c)) / (e - b))
x3 = a - x2
x1 = x2 + (((x3 - x2) / a) *  x0)

A partir de y1 es calcula el punt x2 fent servir l’equació de la recta que passa per (0,239) i (c=120, e=40).

Per simetria horitzontal es calcula x3.

El punt x1 es calcula considerant el mapeig lineal entre (0, 319) i (x2, x3)

9 Optimització i línies en blanc

El mapeig és el mateix per a tots els frames. Aleshores, en comptes de recalcular el mapeig per a cada frame, es pot calcular un cop al començament i mantenir-lo en un array.
Un array, en principi, de 320×240. Però el cas és que el creixement en y1 de la funció de mapeig entre els punts d’un frame (x0,y0) i els punts del trapezi (x1, y1) fa que per a increments d’1 píxel en la variació de y0 el trapezi tingui línies buides a la seva part inferior, o sigui, y1 té “forats”. La solució és fer que els increments en y0 siguin més petits que un píxel, per a que y1 no tingui forats. Fent algunes proves, he vist que amb increments de y0 de 0.5 ja n’hi ha prou per a que la funció de transformació no deixi línies buides.

Al final, doncs, el precàlcul de la transformació es pot fer amb el següent codi:

# create frame transform master
num_steps = 2.0
frame_transform = [["1" for j in xrange(0,int(num_steps))] for i in range(0, w * h_frame)]
for y in xrange(0, h_frame):
    for x in xrange(0, w):
        for inc_y in xrange(0, int(num_steps)):
            frame_transform[x + y * w][inc_y] = transform(x, y + inc_y * (1 / num_steps) )

10 Generació dels frames processats (i afegir el color)

La generació dels frames processats és directa: per a cada frame…

for frame_count in xrange(0, h_img_base - h_frame): 
      filename_frame = filename_pattern % frame_count
      filename_processed_frame = filename_processed_pattern % frame_count

      # read and transform frame 
      with open(frames_folder + filename_frame, "r") as fr:
          # discards three first header lines 
          fr.readline()
          fr.readline()
          fr.readline()

… aplico la transformació,tenint en compte que els increments de y0 seran fraccionaris. Per simplificar, en comptes de generar directament el frame transformat faig servir un array de 320×240 com a pas intermig…

# initialize black frame
frame_bits = ["1" for i in xrange(0, w * h_frame)]

for y in xrange(0, h_frame):
    for x in xrange(0, w):
        bit = fr.read(1)
        if bit == '\n':
            bit = fr.read(1)

        # parabolic. trick for filling gaps
        for inc_y in xrange(0, int(num_steps)):
            (xt, yt) = frame_transform[x + y * w][inc_y]
            if int(xt) < w and int(yt)< h_frame:
                frame_bits[int(xt) + int(yt) * w] = bit

Finalment, escric el frame transformat. Aprofito aquest últim pas per afegir el color ja que durant tot el procés he considerat la imatge només en blanc i negre.

Simplement, genero el frame transformat amb el format PPM

10.1 Format PPM

De la viquipèdia, un exemple de PPM.

P3
# The P3 means colors are in ASCII, then 3 columns and 2 rows,
# then 255 for max color, then RGB triplets
3 2
255
255   0   0     0 255   0     0   0 255
255 255   0   255 255 255     0   0   0

Per tant,

# write transformed frame
# c = 0
rgb = ""
black = " 0 0 0"
yellow = " 255 255 0"  
print "[Processed Frame %0#5d]" % frame_count
with open(processed_frames_folder + filename_processed_frame, "w") as fw:
    fw.write("P3\n")
    fw.write("# CREATOR: Albert Baranguer Codina\n")
    fw.write("%d %d 255\n" % frame_size)
    for bit in frame_bits:
        if bit == "1":
            rgb = black
        else:
            rgb = yellow 
        fw.write(rgb)
        c = c + 1
        if (c == w):
            c = 0
            fw.write('\n')

El resultat de l’execució d’aquest codi és que tindré 560 frames processats a carpeta processed-frames

11 generació del video mp4

Arribats a aquest punt ja podem visualitzar el vídeo fent servir una petita modificació del player1.py que he mostrat abans. Però com que el resultat demanat era un mp4, amb un parell de passos addicionals fent servir convert de imagemagick i ffmpeg obtinc el resultat final.

La idea original era fer servir ffmpeg amb els pbm generats en el pas anterior per a obtenir el vídeo, pero en proves he vist que al ffmpeg no sembla agradar-li aquest format de imatge. O sigui que he transformat els pbm a png amb el següent script: pbm-to-png.sh

#!/bin/bash

for filename in $(ls -b ./processed-frames)
do
    convert ./processed-frames/"$filename" ./png/"$filename".png
done
echo "done!"

I un cop he obtingut els frame en format png a la carpeta png, finalment, he generat l’mp4 amb create-mp4.sh

#!/bin/bash
ffmpeg -i png/frame_%05d.processed.pbm.png \
       -c:v libx264 \
       -pix_fmt yuv420p png/starwars-credits-fx.v1.mp4

I vet aquí el resultat

 

12 Repositori a GitHub

Podeu trobar el codi ue he fet servir al meu repositori de GitHub

https://github.com/abaranguer/sw-rolling-fx

“Revolution OS”, the movie.

És estiu i és moment de relaxar-se.  Avui toca cine: El documental “Revolution OS”.

Vet aquí la ressenya de la pel·lícula que es pot trobar al YouTube:

“Revolution OS is a 2001 documentary which traces the history of GNU, Linux, and the open source and free software movements.

It features several interviews with prominent hackers and entrepreneurs (and hackers-cum-entrepreneurs), including Richard Stallman, Michael Tiemann, Linus Torvalds, Larry Augustin, Eric S. Raymond, Bruce Perens, Frank Hecker and Brian Behlendorf.

The film begins in medias res with an IPO, and then sets the historical stage by showing the beginnings of software development back in the day when software was shared on paper tape for the price of the paper itself.

It then segues to Bill Gates’s Open Letter to Hobbyists in which he asks Computer Hobbyists to not share, but to buy software. (This letter was written by Gates when Microsoft was still based in Arizona and spelled “Micro-Soft”.)

Richard Stallman then explains how and why he left the MIT Lab for Artificial Intelligence in order to devote his life to the development of free software, as well as how he started with the GNU project.

Linus Torvalds is interviewed on his development of the Linux kernel as well as on the GNU/Linux naming controversy and Linux’s further evolution, including its commercialization.

Richard Stallman remarks on some of the ideological aspects of open source vis-á-vis Communism and capitalism and well as on several aspects of the development of GNU/Linux.

Michael Tiemann (interviewed in a desert) tells how he met Stallman and got an early version of Stallman’s GCC and founded Cygnus Solutions.

Larry Augustin tells how he combined the resulting GNU software and a normal PC to create a UNIX-like Workstation which cost one third the price of a workstation by Sun Microsystems even though it was three times as powerful. His narrative includes his early dealings with venture capitalists, the eventual capitalization and commodification of Linux for his own company, VA Linux, and ends with its IPO.

Frank Hecker of Netscape tells how Netscape executives released the source code for Netscape’s browser, one of the signal events which made Open Source a force to be reckoned with by business executives, the mainstream media, and the public at large.

(this text is available under the terms of the GNU Free Documentation License)

I sense més dil·lació, amb tots vosaltres els gurús i hackers que són a l’origen del Linux, del Programari Lliure (el “Free Software”, amb “free as in freedom”) , del sistema GNU i del Free and Open Source Software (FOSS).

Apaguem els llums. “Revolution OS”, the movie:

60 anys de Lisp. Old rockers never die.

El programador no neix. Es fa.

La formació d’un bon programador passa per diverses etapes. En la etapa inicial hom aprèn un llenguatge. Normalment un llenguatge d’iniciació. Un llenguatge d’iniciació serveix, més que res, per comprendre conceptes de programació.  I no és tan important el seu rendiment o la seva aplicació en problemes reals.

Avui, per exemple, es fa servir, a nivell de ESO i secundària, el llenguatge Scratch (un fantàstic i versàtil entorn d’aprenentatge) com a llenguatge d’iniciació.

A nivells més avançats, hi ha més varietat. Sobretot als EUA es fa servir molt -com a llenguatge d’iniciació per a universitaris, repeteixo- el Lisp, en alguna de les seves encarnacions: Scheme, Racket, CLisp

Lisp és proper al llenguatge matemàtic, té una sintaxi mínima i no imposa gaires restriccions en quant a paradigmes de programació, tot plegat el fa molt versàtil per a presentar conceptes informàtics. Alguns dels millors i més influents manuals de programació fan servir el Lisp. Cal llegir al menys una vegada en la vida el llibre porpra: Structure and Interpretation of Computer Programs.

Potser sorprenentment, el Lisp es un llenguatge que es troba amb  facilitat a entorns productius Unix. Guile (Scheme), un altre dialecte de Lisp, és el llenguatge d’extensió oficial de les aplicacions GNU (un llenguatge d’extensió és, per entendre’ns, un llenguatge de macros). També es pot trobar Lisp sent part indestriable i indissoluble de l’Emacs, l’editor de texts més versàtil mai creat. Emacs, en ell mateix, està en molt bona part programat amb Emacs Lisp. La forma d’estendre Emacs és, evidentment, amb l’Emacs Lisp. La presència universal d’Emacs posa el Lisp allà on hi hagi un Emacs instal·lat.

Lisp (inventat per John McCarthy entre 1956 i 1958) és el segon llenguatge informàtic més antic encara en servei (el més antic, encara en servei, és Fortran. La primera especificació de Fortran va ser escrita per IBM a  1954). Aquesta timeline de la wiki us pot ajudar a situar-lo en el temps.

Estem parlant, doncs,  d’un llenguatge informàtic amb 60 anys d’història. Es diu de pressa. En termes informàtics tant de temps equival al pas  de vàries eres geològiques.

Però el millor del cas és que l’evolució del Lisp continua: és relativament senzill programar un interpret de Lisp i això fa que es puguin trobar dialectes per a multitud de plataformes. Concretament, per a la JVM en aquests moment hi ha una versió de Lisp que destaca sobre les altres: Clojure.

Clojure és un dialecte de Lisp que funciona sobre la JVM. Tot i que no imposa el paradigma, presenta una orientació envers la Programació Funcional (FP) [i Clojure FP ]. Val a dir que és possible fer FP, en general, amb dialectes comuns de Lisp.

Com a punt molt fort, es relaciona de forma senzilla amb la JVM, de forma que té accés a la infinitat de llibreries i frameworks de Java i, a l’hora, és senzill des de Java  invocar Clojure, o fer servir des de Java les classes desenvolupades amb Clojure.

Més enllà de la curiositat de portar Lisp a la JVM, sembla que Clojure està fent-se, poc a poc, això sí, amb una part de mercat en l’anàlisi de dades (on a hores d’ara dominen R i Python amb Pandas, sense comptar amb el clàssic SQL, o l’omnipresent Java).

Però, tornant al principi, en la formació d’un programador el primer pas acostuma a ser algun llenguatge d’iniciació que ràpidament es complementa amb l’aprenentatge de llenguatges més utilitzats professionalment i amb llenguatges de paradigmes diferents.

La programació amb diferents paradigmes dona una visió completa al programador. Li obre la ment. L’habilita per enfocar els problemes des de punts de vista diferents. Per això, un bon programador ho és de forma independent als llenguatges que conegui , tot i que, evidentment, el més probable i el més recomanable és que tingui alguns llenguatges preferents  que domini. I també que aquestes preferències vagin canviant amb el pas del temps. El bon programador és poliglot, i sempre té un ull posat en els nous llenguatges i paradigmes que van apareixent.

Dins la caixa d’eines del programador hom pot trobar, doncs,  diferents llenguatges per a diferents propòsits. Alguns llenguatges seran molt demandats professionalment, i altres, no tant, o gens ni mica. Però en l’arsenal mental del programador, aquesta varietat expressiva és riquesa i versatilitat.

Per acabar, l’objectiu del post d’avui era redescobrir Lisp i perquè crec que pot tenir interès dedicar un temps a aquest llenguatge. Com usuari que sóc d’Emacs (Emacs Lisp) i d’eines GNU (Guile i Scheme), Lisp té un caràcter pràctic. Com programador Java i que ha treballat més o menys amb anàlisi de dades, Clojure també em crida l’atenció.

La paraula clau és poliglotisme. Us deixo un enllaç: http://hyperpolyglot.org/. Es tracta d’un lloc web que ofereix taules de comparació entre llenguatges més o menys afins. En concret, us proposo fer un cop d’ull a aquestes taules:

Llenguatges d’Scripting : Node.js, PHP, Python i Ruby. [full 1]i [full 2].

Llenguatges d’anàlisi numèric i estadístic: MATLAB (i Octave), R, NumPy i Julia. [full 1] i [full 2].

Comparació entre dialectes de Lisp: Common Lisp, Racket (Scheme), Clojure, Emacs Lisp .

Cinc problemes de programació

Una de les conseqüències de l’ERO a Indra (on encara treballo) és que a …curt? …mig? termini m’incorporaré al mercat laboral, o el que és el mateix, que hauré de buscar feina. I és bastant probable (i em sembla el més sensat) que la feina que busqui estigui relacionada amb el que sé fer, que és, bàsicament, de programador.

Programador? Alguns diran Analista, o en anglès, Software Engineer, o Software Architect, o títols encara més originals… En essència, però, jo crec que el que és distintiu de la meva feina és que sóc capaç de programar ordinadors i fer-los funcionar junts. Per tant, fem-ho fàcil i digues-me programador. Ras i curt.

El cas és que buscant sobre el que em puc esperar a les entrevistes de feina vaig trobar el següent article: “Five programming problems every Software Engineer should be able to solve in less than 1 hour“. Sí,ho heu entès: cinc problemes de programació que tot enginyer de software hauria de ser capaç de resoldre en menys d’una hora. Em va semblar força interessant.

L’autor d’aquest article diu que quan publica una oferta demanant programadors es troba amb candidats que no saben què vol dir “programar”. Aleshores proposa el que, al seu entendre, són cinc problemes bàsics que tot programador “de veritat” hauria de ser capaç de resoldre en menys d’una hora.

Cinc problemes en una hora. Cal fer algunes consideracions. Què ha de fer un programador per superar un repte d’aquest estil? primer de tot, cal comprendre (analitzar) el problema i les seves implicacions, i dissenyar una solució. Encara que sigui en forma de notes mentals. Hi ha alguna forma de validar el disseny de la solució trobada? Un cop analitzat i dissenyat el problema, cal pensar en la implementació de la solució. Probablement serà més eficient un llenguatge específic del domini del problema que un llenguatge de propòsit general. Si es pot triar, en general és més ràpid desenvolupar una resposta amb un intèrpret que amb un compilador (ni que només sigui perquè s’estalvia el temps de compilat); per descomptat, és més eficient disposar d’un bon entorn de desenvolupament, que proporcioni coses com compleció del codi, acoloriment de sintaxi, snippets de codi, generació automàtica de codi, depurador… que fer servir un editor de texts pelat, per molt que per Internet es trobin acudits tan graciosos com aquell que diu que el millor IDE és vi + gcc.

Per descomptat, més enllà de si interpretat o compilat, és clau per a desenvolupar de forma eficient, el conèixer a fons el llenguatge triat per al desenvolupament. Finalment, l’experiència és un grau. És més probable que algú que ha fet moltes hores de programació s’hagi trobat amb problemes similars en algun moment de la seva trajectòria i que els hagi solucionat. En resum: per a millorar l’eficiència cal experiència i coneixement en el llenguatge i les eines a utilitzar.

A més de tot l’anterior, un programador de veritat sap que l’objecte del seu treball, el codi, ha de tenir unes determinades característiques, com per exemple la legibilitat i l’organització. per no parlar de la gestió de recursos, erros i la generació i gestió de casos de proves.

Per si fos poc, el codi, com producte, s’ha d’administrar i segueix uns fluxos dins de les organitzacions. Al menys com usuari, un programador hauria d’estar al cas dels sistemes de Control de Versions, o dels sistemes d’Integració Continua.

Tot això son coneixements tècnics que ha de tenir el programador. Però no només. El programador hauria de tenir unes competències generals que inclourien la capacitat d’auto-organització (tècniques com GTD), l’autonomia, l’auto-didactisme, la bona capacitat de comunicació, oral i escrita (afegiria també audiovisual), la capacitat de treball en equip, i també capacitat didàctica i de lideratge. I segur que em deixo coses.

Bé. De totes aquestes característiques no no en parla l’autor de l’article. Jo trobo que a l’hora de contractar un programador caldria tenir-les en compte. Però el fet és aquest: el que de debò és imprescindible per a un programador és que sàpiga programar.

Tornant al prinicipi: Quins són aquests cinc problemes que tot programador hauria de poder resoldre en menys d’una hora?

Problem 1
Write three functions that compute the sum of the numbers in a given list using

  • a for-loop
  • a while-loop
  • recursion

Problem 2
Write a function that combines two lists by alternatingly taking elements. For example: given the two lists [a, b, c] and [1, 2, 3], the function should return [a, 1, b, 2, c, 3].

Problem 3
Write a function that computes the list of the first 100 Fibonacci numbers. By definition, the first two numbers in the Fibonacci sequence are 0 and 1, and each subsequent number is the sum of the previous two. As an example, here are the first 10 Fibonnaci numbers: 0, 1, 1, 2, 3, 5, 8, 13, 21, and 34.

Problem 4
Write a function that given a list of non negative integers, arranges them such that they form the largest
possible number. For example, given [50, 2, 1, 9], the largest formed number is 95021.

Problem 5
Write a program that outputs all possibilities to put + or – or nothing between the numbers 1, 2, …, 9 (in this order) such that the result is always 100. For example: 1 + 2 + 34 – 5 + 67 – 8 + 9 = 100.

Déu n’hi do! Els problemes 1,2 i 3 són senzills. Ara bé, el 4 i el 5 poden fer-te rumiar una bona estona.

En una actualització de post, el mateix autor de l’article reconeixia que els problemes 4 i 5 havien generat una important controvèrsia.

Bé, jo també trobo que en una hora els cinc problemes és, més aviat, justet.

En el meu cas, em vaig decidir a fer els problemes. Ara bé, no pretenia resoldre’ls en una hora. De fet, el que pretenia és que em servissin per practicar un llenguatge que em fa molta gràcia i que he “descobert”, per dir-ho d’alguna manera, fa poc: amb l’Elisp de l’Emacs.

Sí, he fet servir lisp per resoldre els problemes, què passa? xD (de fet, el problema 5 l’he fet primer amb Python, però ja en parlaré després)

Allà van les meves solucions:

Solució al problema 1 (amb for-loop –> dotimes)

;; The 5 problems
;;
;; Problem 1
;; Write three functions that compute the sum of the numbers in a given list using 
;;  - a for-loop
;;  - a while-loop
;;  - recursion
;;
;; 1.1 - for-loop (dotimes)
(progn 
  (setq nums (list 1 2 3 4 5 6 7 8 9 10))
  (setq N (length nums))
  (setq suma 0)

  (dotimes (i N) 
    (setq suma (+ suma (elt nums i)))
  )

  (insert (format "\nFor loop (with dotimes) sum: %d" suma))
)
For loop (with dotimes) sum: 55

Solució al problema 1 (amb while-loop)

;; 1.2 - while
(progn 
  (setq nums (list 1 2 3 4 5 6 7 8 9 10))
  (setq N (length nums))
  (setq suma 0)
  (setq i 0)
  (while (< i N) 
    (setq suma (+ suma (elt nums i)))
    (setq i (1+ i))
  )

  (insert (format "\nWhile loop sum: %d" suma))
)
While loop sum: 55

Solució al problema 1 (recursiva)

;; 1.3 - recursion
(defun recursiveSum(numsList)
  (setq N (length numsList))
  
  (if (> N 0)
    (+ (car numsList) (recursiveSum (cdr numsList)))  ;; if part
    0                                                 ;; else part
  )
)

(progn 
  (setq nums (list 1 2 3 4 5 6 7 8 9 10))
  (setq suma 0)

  (setq suma (+ (car nums) (recursiveSum (cdr nums)))) 
  
  (insert (format "\nRecursive sum: %d" suma))
)
Recursive sum: 55

Fàcils, no? Anem pel problema 2:

;; Problem 2
;; Write a function that combines two lists by alternatingly taking elements. 
;; For example: given the two lists [a, b, c] and [1, 2, 3], 
;; the function should return [a, 1, b, 2, c, 3].
;;
(defun merger (listData indexListData listUnion) 
  (push (nth indexListData listData) listUnion)
  listUnion   ;; not strictly necessary
)

(progn 
  ;; test data
  (setq list1 (list "a" "b" "c"))
  (setq list2 (list 1 2 3 4 5 6))
  (setq listUnion (list))

  ;; get length of lists
  (setq lengthList1 (length list1))
  (setq lengthList2 (length list2))
  
  ;; length of union list
  (setq lengthUnion (+ lengthList1 lengthList2))

  (setq indexList1 0)
  (setq indexList2 0)

  (dotimes (i lengthUnion)
    ;; list 1
    (if (< indexList1 lengthList1)
      (setq listUnion (merger list1 indexList1 listUnion)) 
    )
    (setq indexList1 (1+ indexList1))
    
    ;; list 2
    (if (< indexList2 lengthList2)
      (setq listUnion (merger list2 indexList2 listUnion)) 
    )
    (setq indexList2 (1+ indexList2))
  )
 
  ;; reverse
  (setq listReversed (reverse listUnion))

  ;; show results
  (print "List merger")
  (print listReversed)
)

Una mica embolicat, no? No és el millor codi que he escrit, la veritat. El codi s’hauria simplificat si en comptes de fer servir list hagués fet servir vectors.

La funció merger no fa mes que posar al principi de la llista unificada l’element enèsim de la llista passada com argument. Com que els elements de les dues llistes es van posar alternativament al començament (push) de la llista unificada, per això cal fer un reverse de la llista unificada al final.

Bé, amb Python o amb Basic m’hauria sortit millor. Seguim.

Solució del problema 3

;; Problem 3
;; Write a function that computes the list of the first 50 Fibonacci numbers. 
;; By definition, the first two numbers in the Fibonacci sequence are 0 and 1, 
;; and each subsequent number is the sum of the previous two. 
;; As an example, here are the first 10 Fibonnaci numbers: 
;; 0, 1, 1, 2, 3, 5, 8, 13, 21, and 34.
;; 
(progn 
  (setq n0 0.0)
  (setq n1 1.0)
  (insert "\n - 50 first Fibonacci numbers -\n")
  (insert "0\n1\n")
  (dotimes (i 50)
    (setq n (+ n1 n0))
    (insert (format "%d\n" n))
    (setq n0 n1)
    (setq n1 n)
  )
)
 - 50 first Fibonacci numbers -
0
1
1
2
3
5
8
13
21
34
...
701408733
1134903170
1836311903
2971215073
4807526976
7778742049
12586269025
20365011074

Un clàssic. La successió de Fibonacci. Aquest problema també és molt senzill.

Solució (errònia) al problema 4

;; Problem 4
;; Write a function that given a list of non negative integers, arranges them such that they form the largest possible number. 
;; For example, given [50, 2, 1, 9], the largest formed number is 95021.
;; 
(defun greater (lower upper)
  (and (not (string< lower upper)) (not (string= lower upper)))
)

(defun swap (row i)
   (setq aux (aref row i))
   (aset row i (aref row (1+ i)))
   (aset row (1+ i) aux)
)

(defun bubble-sort (strNums)
  (setq N (length strNums))
 
   ;; sort array of strings 
  (dotimes (j (- N 1))
    (dotimes (i (- N 1)) 
      (setq lower (aref strNums i))
      (setq upper (aref strNums (1+ i)))
   
      (if (greater lower upper)
        (swap strNums i)
      )
      ;;(show-row strNums)
    )
  )
)

(defun show-row (strNums)
  (setq N (length strNums))
  (dotimes (i N)
    (insert (format "%s " (aref strNums i )))
  )
  (insert "\n") 
)

(defun reverseVector (row)
  (setq N (length row))
  (dotimes (j (/ N 2))
    (setq val1 (elt row j))
    (setq val2 (elt row (- (- N 1) j)))
      
    (aset row (- (- N 1) j) val1)
    (aset row j val2)
  ) 
)

(defun concatVector (row)
  (setq N (length row))
  (setq retvalue "")
  (dotimes (i N)
    (setq retvalue (concat retvalue (elt row i)))
  )
  retvalue  ;; not strictly necessary 
)

(progn
  ;; data: list of non negative integers
  (setq nums (list 9 8 7 6 5 4 3 2 1 0))
 
  ;; convert to array of strings
  (setq N (length nums))
  (setq strNums (make-vector N nil))
  (dotimes (i N) 
    (aset strNums i (number-to-string (elt nums i)))
  )

  (insert "\nGreatest integer\n")
  (insert "\nunsorted array:\n")  
  (show-row strNums)

  (insert "\nsorted array:\n") 
  (bubble-sort strNums)
  (show-row strNums)

  (insert "\nreversed sorted array:\n") 
  (reverseVector strNums)
  (show-row strNums)
 
  ;; string to number
  (setq greatestNumber (string-to-number (concatVector strNums)))
  (insert (format "Greatest integer: %d\n" greatestNumber)) 
)
Greatest integer

unsorted array:
9 8 7 6 5 4 3 2 1 0 

sorted array:
0 1 2 3 4 5 6 7 8 9 

reversed sorted array:
9 8 7 6 5 4 3 2 1 0 
Greatest integer: 9876543210

Doncs bé, resulta que la solució no és correcta! He suposat que n’hi havia prou amb ordenar (funció bubble-sort, com no!) alfabèticament els números, però resulta que aquesta suposició és falsa, com indiquen en aquest altre post:

“A lot of folks have rightly pointed out that this problem can be solved using a lexical comparison of strings. However, that’s not all you need.

Consider the following example: [5, 50, 56]. A lexical comparison returns 56, 50, and 5, but that doesn’t make the larger number (56, 5, 50 does)”

O sigui que: meeec! cal rumiar un altre cop la solució del problema 4. Queda pendent la solució per a un futur post.

UPDATE: Solució Brute Force:

Ahir no estava d’humor per a posar-me a investigar un algorisme pera solucionar el problema 4… i avui tampoc! L’opció és fer força bruta, que,de fet, als ordinadors se’ls dona molt bé.

Amb força bruta i generadors de Python (itertools) la cosa queda tan simple com això:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# biggetst integer

import itertools as itt

print "\nBiggest number\n"

nums = ['500', '5', '51', '52', '504', '506', '54', '56', '514', '515', '516', '575']

max_str = ''
num_iters = 0

for perm in itt.permutations(nums, len(nums)):
    num_iters = num_iters + 1
    num = ''.join(perm)
    if  num > max_str:
        max_str = num

print "max: %s; iters: %d\n" % (max_str, num_iters)        

'''
>>> import biggest

Biggest number

max: 575565545251651551514506504500; iters: 479001600
''' 

Aquesta solució és qualsevol cosa menys elegant, però és molt ràpida de programar i soluciona el problema en un temps finit. Cal dir que he tirat, de nou, de la màgia de Pyhton que m’ha permès resoldre amb el simple import de les itertools la generació de les permutacions dels números. Després la concatenació amb el join i actualitzar el màxim a mida que es va trobant.

Queda pendent, per a quan tingui temps i ganes d’investigar-ho, el trobar un algorisme més intel·ligent que la solució per força bruta. Això sí, en tot cas, la força bruta sempre és una opció a considerar (i els bons programadors ho saben).

Finalment, la solució al problema 5

Primer la solució amb Python. I ara veureu perquè Python és el Llenguatge dels Déus:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Problem 5
Write a program that outputs all possibilities to put + or - or nothing 
between the numbers 1, 2, ..., 9 (in this order) 
such that the result is always 100. 
For example: 1 + 2 + 34 - 5 + 67 - 8 + 9 = 100.
'''
def get_base3_indexes(num):
    ix = [0,0,0,0,0,0,0,0]
    done = False
    i = 0
    while not done:
        quot, mod = num / 3, num % 3

        if quot >= 3:
            num = quot
            ix[i] = mod
        else:
            done = True
            ix[i] = mod
            ix[i + 1] = quot

        i = i + 1

    ix.reverse()
    return ix

if __name__ == "__main__":
    symbols = ["+","-"," "]
    formula = "1%c2%c3%c4%c5%c6%c7%c8%c9"
    ix = [0,0,0,0,0,0,0,0]
     
    # és a dir: Variacions amb Repetició de 3 elements agafats de 8 en 8
    # RV n m = n ** m
 
    top =  3**8
    
    for i in range(0, top - 1):
        ix = get_base3_indexes(i)
        
        calc = formula % (symbols[ix[0]],
                          symbols[ix[1]],
                          symbols[ix[2]],
                          symbols[ix[3]],
                          symbols[ix[4]],
                          symbols[ix[5]],
                          symbols[ix[6]],
                          symbols[ix[7]])

        calc2 = [c for c in calc if c != " "]
        calc2_compact = "".join(calc2)
        result = eval(calc2_compact)
        if result == 100:
            print "%s = %d" % (calc2_compact, result)
            
    print "%d lines analyzed" % top
    print "done!"

O sigui, calcula les “variacions amb repetició de tres elements agafats de 8 en 8″ Els tres elements són les tres “operacions” possibles entre dígits: sumar (“1” + “2” = 3), restar (“1” – “2” = -1) i concatenar (“1” concat “2” = “12”) i els agafo de 8 en 8 perquè són 8 posicions per a operadors entre els 9 operands numèrics. la iteració per tots els casos possibles i la conversió a base 3 de l’index actual permet posar els operadors adequats on els toca en cada iteració.

Però la màgia de Python ve en aquestes tres línies:

        calc2 = [c for c in calc if c != " "]
        calc2_compact = "".join(calc2)
        result = eval(calc2_compact)

La primera línia fa una llista d’elements (calc2) a partir de la llista original (calc) on els operadors “concat” apareixen com blancs entre els números. Agafa cada element c de calc excepte els elements iguals a ” “. És dir, “compacta” la llista calc eliminant-li els blancs. Es tracta d’un bonic exemple de Python Comprehensions.

La següent línea genera un string a partir de la llista compactada.

la tercera línia avalua la suma.

La dada és que fa tot això en tres línies. Eficiència i productivitat en un llenguatge és exactament això.

I ara l’equivalent amb elisp:

;; Problem 5
;; Write a program that outputs all possibilities to put + or - or nothing 
;; between the numbers 1, 2, ..., 9 (in this order) 
;; such that the result is always 100. 
;; For example: 1 + 2 + 34 - 5 + 67 - 8 + 9 = 100.
;; 
;; variations with repetition of 3 elements (the +,- and "concat" operators) 
;; taken 8 (8 operands within the 1,2,3,4,5,6,7,8 and 9 operands) at a time
;;
;; VR n r = n ** r
;;  
(defun main ()
  ;; return vector with coeficients of the conversion of num to base 3.
  (defun get_base3 (num)
    (let
      ( 
        (base3 (make-vector 8 0))
        (n 8)
        (done nil)
        (i 0)
        (quot 0)
        (mod 0)
      )
      
      (while (not done)
        (setq quot (/ num 3))
        (setq mod (% num 3))

        (if (>= quot 3)
          (progn
            (setq num quot)
            (aset base3 i mod)
        
          )
          (progn
            (setq done t)
            (aset base3 i mod)
            (aset base3 (1+ i) quot)
          ) 
        )
        (setq i (1+ i))
      )

      ;; return
      base3
    )
  )

  ;; clean white spaces
  (defun clean_white_spc (str)
    (let
      ( 
        (c "")
        (j 0)
        (str_out "")
        (len (length str))
      )
      (dotimes (j len)
        (setq c (substring str j (+ j 1)))
        (if (not (string= c " "))
          (setq str_out (concat str_out c))
        )
      )

      ;; return
      str_out
    )
  )

  ;; formula parser
  (defun perform_calculation (formula)
    (let 
      (
        (n (length formula))
        (stackoperands (make-vector 9 0))
        (stackoperators (make-vector 8 "+"))
        (i 0)
        (j 0)
        (k 0)
        (accum 0)
      )
 
      ;; lexical parser
      (dotimes (i n)
        (setq c (substring formula i (1+ i)))
        (if (and (not (string= c "+")) (not (string= c "-")))
          (progn ;; true. c is a number
            (setq accum (+ (* accum 10) (string-to-number c)))
          ) 
          (progn ;; false. c is an operator  
            (aset stackoperands j accum)
            (aset stackoperators k c)
            (setq k (1+ k))
            (setq j (1+ j))
            (setq accum 0)
          )
        )
        (if (> accum  0)
          (aset stackoperands j accum)
        )
      )

      ;; performs the sum
      (setq i 0)
      (setq j 0)
      (setq operator "")
      (setq operand 0)
      (setq accum 0)

      (dotimes (i (length stackoperands))
        (setq operand (aref stackoperands i))
        (if (= i 0)
          (progn ;; i = 0
            (setq accum operand) 
          )
          (progn ;; i > 0
            (setq j (1- i))
            (setq operator (aref stackoperators j))
            (if (string= operator "+")
              (setq accum (+ accum operand))
              (setq accum (- accum operand))
            )
          )
        )
      )

      ;; return
      accum
    )
  )

  ;; main
  (let
    (
      (symbols ["+" "-" " "])
      (formula "1%s2%s3%s4%s5%s6%s7%s8%s9")
      (ix (make-vector 8 0)) 
      (top (- (expt 3 8) 1))
      (i 0)
      (count 0)
    )
  
    (insert "\nSum 100\n")
 
    (dotimes(i top) 
      (setq ix (get_base3 i))
      (setq calc (format formula (aref symbols (aref ix 0))
                                 (aref symbols (aref ix 1))
                                 (aref symbols (aref ix 2))
                                 (aref symbols (aref ix 3))
                                 (aref symbols (aref ix 4))
                                 (aref symbols (aref ix 5))
                                 (aref symbols (aref ix 6))
                                 (aref symbols (aref ix 7))  
                 )
      )
     
      (setq calc2 (clean_white_spc calc))
      (setq sum (perform_calculation calc2))

      (if (= sum 100)
        (progn 
          (setq count (1+ count))
          (insert (format "%d: %s = %d\n" count calc2 sum))
        )
      )
    )
    (insert (format "\n%d lines analyzed\ndone!"  top))
  )
)

És equivalent a la solució amb Pyhton, només que ha calgut fer funcions per a fer la compactació i eliminació dels espais en blanc, i també per avaluar la fórmula amb un senzill parser.

……

Una hora diu. En fi.

WebService amb wsgen/wsimport i Maven

WebServices amb Maven

1 Crear un webservice a partir d’una classe java

1.1 Preparació

Aquest és un post que tenia ganes d’escriure de feia temps: Com fer un webservice amb java, i com invocar-lo.

El entorn que faré servir per a desenvolupar el webservice és:

Comencem. És una bona pràctica encetar el desenvolupament a partir d’un archetype Maven. Trio l’archetype d’aplicació webapp-javaee6.

mvn archetype:generate \ 
   -DgroupId=cat.apuntstecnologia.proves \
   -DartifactId=jeews \
   -DarchetypeGroupId=org.codehaus.mojo.archetypes \
   -DarchetypeArtifactId=webapp-javaee6

Que genera la següent estructura:

project
|-- pom.xml
`-- src
    `-- main
        |-- java
        `-- webapp
            |-- WEB-INF
            |   `-- web.xml
            `-- index.jsp

Tanmateix també hauria estat una bona tria començar amb l’archetype de webapp. Mancaria afegir la carpeta java i modificar el pom.xml.

mvn archetype:generate \
    -DgroupId=cat.apuntstecnologia.proves \
    -DartifactId=warws \
    -DarchetypeArtifactId=maven-archetype-webapp

Un projecte de webapp simple genera l’estructura següent:

project
|-- pom.xml
`-- src
    `-- main
        `-- webapp
            |-- WEB-INF
            |   `-- web.xml
            `-- index.jsp

O fins i tot un simple projecte estàndard de java. Aleshores hauria d’afegir encara més modificacions:

mvn archetype:generate -DgroupId=cat.apuntstecnologia.proves \
                       -DartifactId=jarws \
                       -DarchetypeArtifactId=maven-archetype-quickstart

que és equivalent l’archetype per defecte i és el mateix que:

mvn archetype:generate -DgroupId=cat.apuntstecnologia.proves \
                       -DartifactId=jarws \

Si ho prefereixo, puc treballar amb Eclipse. El projecte Maven es pot convertir a Eclipse amb:

mvn eclipse:eclipse

I, a continuació, puc importar el projecte creat al workspace de l’Eclipse.

1.2 La classe java del servei

Ja sigui amb Eclipse o amb un altre editor, creo la classe del servei web:

package cat.apuntstecnologia.proves;

import java.text.SimpleDateFormat;
import java.util.Date;

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService
public class ServeiProva {
    public ServeiProva() {
    }
    
    @WebMethod
    public int suma(int a, int b) {
    	return a + b;
    } 
    
    @WebMethod
    public String concatena(String a, String b) {
    	return a + b;
    }
    
    @WebMethod
    public String quinDiaEsAvui() {
    	SimpleDateFormat sdf = new SimpleDateFormat("DD/MM/YYYY");
    	return sdf.format(new Date());
    }
}

Un cop tinc el font, compilo la classe:

mvn compile

1.3 wsgen per a generar els objectes del servei

Ara faig servir wsgen sobre la classe per a generar els objectes del web service. wsgen és una eina estàndard que es proporciona amb el JDK.

Primer creo la carpeta src/main/resources. un cop he creat la carpeta em situo a l’arrel del projecte i faig:

wsgen -cp target/classes cat.apuntstecnologia.proves.ServeiProva \ 
      -s src/main/java/ \
      -r src/main/resources/ \
      -d target/classes \
      -wsdl

Amb l’instrucció anterior, ha generat la carpeta

{workspace}/jeews/src/main/java/cat/apuntstecnologia/proves/jaxws

Amb el següent contingut

Concatena.java
ConcatenaResponse.java
QuinDiaEsAvui.java
QuinDiaEsAvuiResponse.java
Suma.java
SumaResponse.java

i també el package amb els .class corresponent a la carpeta target/classes

A més, ha generat el wsdl i l’schema xsd a la carpeta src/main/resources

1.4 Generar el war

En aquest punt ja només queda preparar el paquet war per al seu desplegament com a webservice

Per això a la carpeta src/main/webapp/ creo la subcarpeta WEB-INF i el fitxer web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, 
Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">

<web-app>
    <display-name>ServeiProva</display-name>
</web-app>

i genero el war

mvn package

Que obté, finalment, el war:

/home/albert/workspace/java/jeews/target/jeews-1.0-SNAPSHOT.war

1.5 Deploy al WildFly 9

El renombro a jeews.war i, finalment, faig el desplegament. En el meu cas estic utilitzant el servidor WildFly 9. Per a desplegar en aquest servidor nomes he de deixar el jeews.war a la carpeta

/home/albert/wildfly-9.0.2/standalone/deployments

i fa el deploy automàticament:

17:19:48,943 INFO  [org.jboss.ws.cxf.metadata] (MSC service thread 1-3)
 JBWS024061: Adding service endpoint metadata: 
 id=cat.apuntstecnologia.proves.ServeiProva
 address=http://localhost:8080/jeews/ServeiProva
 implementor=cat.apuntstecnologia.proves.ServeiProva
 serviceName={http://proves.apuntstecnologia.cat/}ServeiProvaService
 portName={http://proves.apuntstecnologia.cat/}ServeiProvaPort
 annotationWsdlLocation=null
 wsdlLocationOverride=null
 mtomEnabled=false

17:19:53,876 INFO  
[org.apache.cxf.wsdl.service.factory.ReflectionServiceFactoryBean] 
(MSC service thread 1-3) 
Creating Service {http://proves.apuntstecnologia.cat/}ServeiProvaService 
from class cat.apuntstecnologia.proves.ServeiProva

17:19:56,321 INFO  [org.apache.cxf.endpoint.ServerImpl] 
(MSC service thread 1-3) 
Setting the server's publish address to be 
http://localhost:8080/jeews/ServeiProva

17:19:56,912 INFO  [org.jboss.ws.cxf.deployment] (MSC service thread 1-3)
 JBWS024074: WSDL published to:
file:/wildfly-9.0.2/standalone/data/wsdl/jeews.war/ServeiProvaService.wsdl

I ara provo el meu web service. Obro un navegador i accedeixo a la URL http://localhost:8080/jeews/ServeiProva?wsdl

i obtinc el wsdl.

1.6 Provar el servei amb SoapUI

Si faig servir aquesta URL amb una eina com SoapUI puc provar els diferents mètodes que exposa el servei.

A la imatge adjunta, veig com s’ha executat l’operació de suma.

 

Figure 1: Prova del webservice amb SoapUI

web-service-soap-ui

2 Crear un client de webservice a partir d’un WSDL

2.1 Preparació

Al punt anterior he fet la prova del servei amb l’aplicació Soap UI. Soap UI és capaç de generar un client del servei web a partir de l’URI del WSDL. El que faré a continuació és això mateix: generar un client del servei web a partir del WSDL.

Com abans, el primer pas és crear un projecte de webapp. Faig servir el mateix archetype que al punt 1.1, el webapp-javaee6.

mvn archetype:generate \ 
   -DgroupId=cat.apuntstecnologia.proves \
   -DartifactId=jeeclientws \
   -DarchetypeGroupId=org.codehaus.mojo.archetypes \
   -DarchetypeArtifactId=webapp-javaee6

2.2 wsimport per a generar les classes del client.

Ara he de generar les classes del client. Faig servir wsimport. Igual que wsgen, wsimport és una eina estàndard del jdk.

wsimport -s src/main/java/ \
         -d target/classes \
         -p cat.apuntstecnologia.proves.jeeclientws \
         http://localhost:8080/jeews/ServeiProva?wsdl

La instrucció anterior ha creat a la carpeta src/main/java el package cat.apuntstecnologia.proves.jeeclientws on ha desat les fonts de les classes java de la implementació del client:

Concatena.java
ConcatenaResponse.java
ObjectFactory.java
package-info.java
QuinDiaEsAvui.java
QuinDiaEsAvuiResponse.java
ServeiProva.java
ServeiProvaService.java
Suma.java
SumaResponse.java

A la carpeta target/classes, per la seva banda, hi són els fitxers .class i l’estructura de carpetes corresponents al package.

En aquest punt ja tinc la implementació de les classes del client. Ara preparo una pàgina jsp per invocar els diferents mètodes del servei, i una pàgina de resposta.

Es tracta dels tres mètodes

int suma(int a, int b);
String concatena(String a, String b);
String quinDiaEsAvui();

Vet aquí el formulari (index.jsp)

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>jeeclientws</title>
	
        function sendRequest(arg) {
            f1.methodInvoked.value = arg;    
            f1.submit();
        }
        
    </head>
    <body>
        <h1>WebService ServeiProva Client</h1>
	<form name="f1" method="post" action="response.jsp">
	    <input type="hidden" name="methodInvoked" />
	    <table>
	        <thead>
		    <tr><td>method</td><td>arg1</td><td>arg2</td><td>&nbsp</td></tr>
		</thead>
		<tbody>
		    <tr>
		        <td>Suma</td>
			<td><input type="text" name="sumArg1" /></td>
			<td><input type="text" name="sumArg2" /></td>
			<td><input type="button" value="send"
			           onclick="javascript:sendRequest('suma');">
		        </td>
		    </tr>
		    <tr>
		        <td>Concatena</td>  
			<td><input type="text" name="concatArg1" /></td>
			<td><input type="text" name="concatArg2" /></td>
			<td><input type="button" value="send"
			           onclick="javascript:sendRequest('concatena');">
		        </td>
		    </tr>
		    <tr>
		        <td colspan="3">Quin dia és avui?</td>
			<td><input type="button" value="send"
			           onclick="javascript:sendRequest('quindia');">			
		    </tr>
		</tbody>
		<tfoot>
		    <tr>
		        <td colspan="4"><input type="reset" value="reset" />
		    </tr>
		</tfoot>
            </table>
	</form>
    </body>
</html>

i el més interessant response.jsp, que és on es fa la invocació propiament dita al webservice

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@page import="cat.apuntstecnologia.proves.jeeclientws.*"%>
<%
String methodInvoked = request.getParameter("methodInvoked");
String sumArg1 = request.getParameter("sumArg1");
String sumArg2 = request.getParameter("sumArg2");
String concatArg1 = request.getParameter("concatArg1");
String concatArg2 = request.getParameter("concatArg2");
String result = "";

ServeiProvaService serveiProvaService = new ServeiProvaService();
ServeiProva serveiProva = serveiProvaService.getServeiProvaPort();

if ("suma".equals(methodInvoked))  {
    result = "" + serveiProva.suma(Integer.parseInt(sumArg1), 
                                   Integer.parseInt(sumArg2));      
}

if ("concatena".equals(methodInvoked))  {
    result = serveiProva.concatena(concatArg1, concatArg2);      
}

if ("quindia".equals(methodInvoked))  {
    result = serveiProva.quinDiaEsAvui();      
}
%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>jeeclientws response</title>
    </head>
    <body>
        <h1>WebService ServeiProva Client - Response</h1>
	<table>
	    <thead>
	        <tr><td>method invoked</td><td>response</td></tr>
	    </thead>
	    <tbody>
                <tr><td><%= methodInvoked%></td><td><%= result%></td></tr>
	    </tbody>
            <tfoot>
                <tr><td colspan="2"><a href="index.jsp">back</a></td></tr>
            </tfoot>
        </table>
    </body>
</html>

I l’execució:

request

Figure 2: Prova del mètode ‘concatena’

response

Figure 3: Resposta del webservice

3 Conclusió. Repositoris GitHub

He creat un web service, i el corresponent client, a partir d’una classe java fent servir les eines wsgen i wsimport que proporciona el jdk; i archetytpes de Maven per a generar les aplicacions.

La generació del codi java del webservice i del client amb les eines ha estat directa i em permet centrar-me en el desenvolupament de les funcionalitats del webservice, més que no pas en la “lampisteria” per posar el webservice en funcionament.

El codi del webservice i del client es pot trobar al meu github, a les adreces:

Webservice (projecte jeews): https://github.com/abaranguer/jeews.git

Client del Webservice (projecte jeeclientws): https://github.com/abaranguer/jeeclientws.git

Aquest article es pot descarregar en format PDF des de http://abaranguer.eresmas.com/webservices.pdf

Author: Albert Baranguer Codina

Created: 2016-01-31 dg 20:54

Emacs 24.5.1 (Org mode 8.2.10)

Validate

Una aplicació de qüestionaris amb Python, GTK (Glade) i SQLite (Star Wars Quizz!)

Motivació

Sempre pot ser útil una aplicació per a fer qüestionaris, o tests. Avui presento el que podria ser l’esquema inicial, la maqueta, d’una aplicació de qüestionaris.

En realitat, la cosa és més divertida: No cal que us recordi que avui, divendres 18 de desembre de 2015, és l’estrena mundial de “Star Wars: The Force Awakens”. Aquesta és una data esperada per molts fans i, en particular, pel meu fill. Fa uns dies el meu fill va preparar un qüestionari, amb Scratch 1.4, sobre l’univers Star Wars. El qüestionari amb Scratch li va quedar força bé i, inesperadament per ell, jo vaig obtenir la màxima puntuació en fer-lo. Va ser el moment per tenir una conversa, de pare a fill, sobre com es poden fer aquests programes de preguntes i respostes. El resultat és que jo li vaig preparar a ell un qüestionari sobre Star Wars, en mode text, amb Pyhton.

A partir d’aquí vaig pensar en sofisticar-ho una mica, desant les preguntes a una base de dades i fent servir una GUI, en comptes del mode text. El resultat és aquesta maqueta d’aplicació de qüestionaris, que originalment era un qüestionari sobre Star Wars i que, per això, és diu Star Wars Quizz! Quines coses, oi? fet aquest aclariment, només ens resta començar i… que la Força ens acompanyi!

 

Programari utilitzat

He triat per a desenvolupar l’aplicació el llenguatge Python, amb Emacs fent de IDE. He fet servir el constructor d’interfícies gràfiques Glade per a fer el disseny de les finestres de l’aplicació. La combinació de Python i Glade (GTK) és, a la pràctica, un entorn ràpid de desenvolupament.

La base de dades de l’aplicació és SQLite3.

El sistema operatiu és Lubuntu 15.10

 

Descripció funcional

Finestra principal i menú de l’aplicació

L’aplicació és molt senzilla: Es presenta com una finestra amb una barra de menús que conté  dos menús desplegables: “Arxiu” i “Ajuda”.

A “Arxiu” hi ha l’entrada “Nou”, que posa en marxa el qüestionari; i l’entrada “Surt”, que tanca l’aplicació.

A “Ajuda” hi ha l’entrada “Quant a” que presenta una finestra amb el logo de l’aplicació, crèdits i llicència.

 

Diàleg de nom i curs

Quan es fa click a “Nou” s’obre una finestra de diàleg en la que se’ns demana el nom de l’usuari i quin curs fa. Hi han dos botons, “Acceptar” i “Tancar”.

Amb “Acceptar” es prenen les dades introduïdes als camps de text, es tanca la finestra que demana el nom i el curs i es mostra el diàleg amb la primera pregunta.

Amb “Tancar” es tanca el diàleg de nom i curs i es descarta la informació que s’hagués introduït.

 

Diàleg de preguntes

La finestra de diàleg que mostra les preguntes s’encapçala amb el nom de l’usuari i el curs que fa. S’indica quin és el número de pregunta que s’està fent.  En cursiva es mostra la pregunta i, a continuació, quatre possibles respostes. L’usuari marca la resposta, o respostes, correctes activant els checkboxes corresponents.

Fent click en “Següent”, es carrega la següent pregunta si n’hi ha. Si s’ha completat el qüestionari es tanca el diàleg de preguntes i s’obre el diàleg que mostra els resultats del qüestionari.

Fent click a “Tancar” es tanca el diàleg de preguntes i respostes, i es descarta nom, curs i el resultat que s’hagués obtingut fins el moment.

 

Diàleg de resultats

Finalment, la pantalla que mostra els resultats indica el nom i curs de l’usuari i mostra quantes preguntes s’han encertat del total.

La pantalla de resultats té un botó de “Tancar” amb el que  es tanca el diàleg de resultat, es descarta nom, curs i el resultat que s’hagués obtingut fins el moment.

Taules

El primer pas ha estat desenvolupar les taules que suporten la funcionalitat  descrita. Són les taules següents:

CREATE TABLE "topics_questions" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    "id_topic" INTEGER NOT NULL
);

CREATE TABLE "topics" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
);

CREATE TABLE "questions_answers" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    "id_question" INTEGER NOT NULL,
    "id_answer" INTEGER NOT NULL
);

CREATE TABLE "questions" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    "question" TEXT
);

CREATE TABLE "question_right_answer" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    "id_question" INTEGER NOT NULL,
    "id_right_answer" INTEGER NOT NULL
);

CREATE TABLE "answers" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    "answer" TEXT
);

No faig servir totes les taules. Algunes estan pensant en futures ampliacions de funcionalitat. Les taules importants són QUESTIONS, que manté les preguntes; ANSWERS, les respostes; QUESTIONS_ANSWERS, taula de relació m-n entre preguntes i respostes; QUESTION_RIGHT_ANSWER, taula m-n entre preguntes i respostes correctes.

Ara mateix només hi ha preguntes sobre Star Wars i llavors es pot simplificar encara més: no he fet servir TOPICS, taula que manté els temes dels qüestionaris; ni TOPICS_QUESTIONS, m-n que relaciona temes i preguntes. Però les taules hi són.

db_loader

Val a dir que la versió original del programa feia servir un questionari mantingut en un parell de llistes de Python. Aleshores, el que he fet és partir d’aquestes llistes per a fer la càrrega inicial de les taules. L’script de càrrega de les taules de l’SQLite ha estat el següent:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sqlite3

questions = (
    u"Com es diu el wookie que acompanya a Han Solo?",
    u"De quina especie són els ossets peluts de la lluna santuari d'Endor?",
    u"A quina batalla va ser destruida la primera Estrella de la Mort?",
    u"Quantes temporades té la serie de The Clone Wars?",
    u"Com es diu la padawan d'Anakin Skywalker?",
    u"Quin és el nom Sith del Comte Dooku?",
    u"De quin planeta és Luke Skywalker?",
    u"A quin planeta és el duel entre Anakin Skywalker i Obi-Wan Kenobi?",
    u"De quin color és l'espasa laser de Mace Windu?",
    u"De quin color és l'espasa laser dels lords Sith?"
)

answers = (
    (2, "Peret", "Chewbacca", "Sr. Spock", "Leia Organa"),
    (1, "Ewoks", "Wookies", "Mandalorians", "Corellians"),
    (4, "Waterloo", "Normandia", "Toth", "Yavin-4"),
    (3, "1000", "5", "6", "7"),
    (3, "Artiom", "Assaj Ventress", "Ahsoka Tano", "Kanan Jarrus"),
    (4, "Darth Sidious", "Darth Turo", "Darth Tenebre", "Darth Tyranus"),
    (2, "Naboo", "Tatooine", "Mandalore", "Alderaan"),
    (1, "Mustafar", "Kamino", "Vulcano", "Warsoom"),
    (4, "Blau", "Verd", "Vermell", "Violeta"),
    (2, "Blanc", "Vermell", "Blau", "Violeta")
)

CONNECTION_STRING = "/home/albert/workspace/python/quizz/db/quizz.db"
INSERT_QUESTION = "insert into questions(question) values (?)"
INSERT_ANSWER = "insert into answers(answer) values (?)"
INSERT_QUESTION_ANSWER = "insert into questions_answers(id_question, id_answer) values (?, ?)"
INSERT_RIGHT_ANSWER = "insert into question_right_answer(id_question, id_right_answer) values (?,?)"

if __name__ == '__main__':
    print "begin"
    conn = sqlite3.connect(CONNECTION_STRING)

    cur = conn.cursor()
    num_question = 0
    
    for question in questions:
        cur.execute(INSERT_QUESTION, (question,))
        id_question = cur.lastrowid

        right_answer = answers[num_question][0]

        for num_answer in range(1,5):
            cur.execute(INSERT_ANSWER, (answers[num_question][num_answer],))
            id_answer = cur.lastrowid

            cur.execute(INSERT_QUESTION_ANSWER, (id_question, id_answer))
            if num_answer == right_answer:
                cur.execute(INSERT_RIGHT_ANSWER, (id_question, id_answer))

        num_question = num_question + 1
        
    conn.commit()
    conn.close()

    print "done!"

El que és interessant de l’script anterior és la llista answers. Es tracta d’una “llista de llistes” (un array multidimensional, en definitiva). En aquesta llista de llistes l’element answers[numPregunta][0] ens diu quin és l’índex de la resposta correcta de la pregunta numPregunta. No he fet una traducció directa de l’estructura de les llistes questions i answers a taules si no que he plantejat unes entitats totalment normalitzades. En la pràctica és el dona més flexibilitat per evolucionar l’aplicació.

Arquitectura de l’aplicació

L’aplicació és molt senzilla i s’hauria pogut resoldre amb un únic mòdul. Però crec que és una bona idea mantenir els bons costums sempre i, per això, he tractat de seguir algunes “bones pràctiques” pel que fa a arquitectura. Essencialment, he procurat seguir el patró MVC i he separat la funcionalitat en quatre mòduls.

quizz.py

Aquest mòdul no fa res més que instanciar el controlador i iniciar l’aplicació.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from controller import Controller

if __name__ == "__main__":
    controller = Controller()
    controller.initApp()

controller.py

El controlador s’implementa amb la classe Controller, molt senzilla, que veu tant la classe GUI com la classe Loader. Realment aquest controlador fa poc més que de pont entre la GUI i les dades carregades amb el Loader. Això és perquè el pas de pantalles està codificat directament als mètodes dels events de la GUI. Seria una bona idea, des del punt de vista d’arquitectura, portar aquesta navegació entre pantalles al controlador.

El codi és el següent:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from gui import GUI
from db import Loader


class Controller():
    gui = None
    db = None
    
    def __init__(self):
        self.db = Loader()
        self.gui = GUI()
        self.gui.setController(self)

    def initApp(self):
        self.db.loadData()
        self.gui.initApp()

    def getQuestion(self, numQuestion):
        return self.db.getQuestion(numQuestion)

    def getNumQuestions(self):
        return self.db.getNumQuestions()

dp.py

Al mòdul db.py implemento la classe Loader. Aquesta classe fa el procés invers de l’script db_loader.py: construeix les llistes questions i answers a partir de les taules de l’aplicació. A més, també afegeix uns mètodes per obtenir el nombre total de preguntes carregades, i per obtenir una pregunta, les seves respostes i la resposta correcta.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sqlite3

class Loader:
    CONNECTION_STRING = "/home/albert/workspace/python/quizz2/db/quizz.db"
    SELECT_QUESTIONS = "select id, question from questions"
    SELECT_ANSWERS = "select a.id, a.answer " + \
                     " from answers a, questions_answers qa " + \
                     " where qa.id_question = ? " + \
                     " and qa.id_answer = a.id"
    SELECT_RIGHT_ANSWER = "select id_right_answer " + \
                          " from question_right_answer " + \
                          " where id_question = ? "

    questions = []
    answer = []
    answers = []
    numQuestions = 0
    
    def loadData(self):
        conn = sqlite3.connect(self.CONNECTION_STRING)
        cur_questions = conn.cursor()
        cur_answers = conn.cursor()
        cur_right_answer = conn.cursor()
    
        cur_questions.execute(self.SELECT_QUESTIONS)
        self.numQuestions = 0
        for row_question in cur_questions:
            # load question
            self.questions.append(row_question[1])
            # load right answer
            cur_right_answer.execute(self.SELECT_RIGHT_ANSWER, (row_question[0],))
            id_right_answer = cur_right_answer.fetchone()[0]
            self.answer = []
            self.answer.append(id_right_answer)
            # load answers for this question
            cur_answers.execute(self.SELECT_ANSWERS, (row_question[0],))
            num_right_answer = 1
            for r_answer in cur_answers:
                self.answer.append(r_answer[1])
                if id_right_answer == r_answer[0]:
                    self.answer[0] = num_right_answer
                num_right_answer = num_right_answer + 1
            self.answers.append(self.answer)
            self.numQuestions = self.numQuestions + 1
        conn.close()
    
    def getQuestion(self, numQuestion):
        return [self.questions[numQuestion-1],
                self.answers[numQuestion-1],
                self.answers[numQuestion-1][0]] 

    def getNumQuestions(self):
        return self.numQuestions
        

Remarcar la utilització de les classes de connexió i de cursor. La paraula Cursor em fa pensar immediatament en els cursors de bases de dades, però a Python la classe Cursors cumpleix més missions que les d’un cursor de base de dades.

GUI.py

Finalment, implemento la classe GUI al mòdul gui.py. Aquesta classe implementa els mètodes de resposta a events de la interfícia gràfica. Tota la interfície gràfica està codificada en un fitxer xml (amb extensió .glade) ies construeix amb Gtk.builder().

L’accés als diferents objectes de la interfícies es fa per nom amb invocacions al mètode get_object(). Amb la referència a l’objecte, es poden fer servir tots els seus mètodes. El procés és molt senzill. Vet aquí el codi:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

from gi.repository import Gtk

class GUI():
    builder = None
    formMain = None
    formNew =None
    formQuestion = None
    formScoring =None
    formAbout = None
    name = ""
    course = ""
    numQuestion = 1
    numQuestions = 0
    rightAnswer = 0
    rightAnswersCounter = 0
    controller = None
    FORMS = "/home/albert/workspace/python/quizz2/forms.glade"

    def setController(self, controller):
        self.controller = controller

    def initApp(self):
        self.builder = Gtk.Builder()
        self.builder.add_from_file(self.FORMS)
        self.builder.connect_signals(self)
        
        self.formMain = self.builder.get_object("applicationwindow1")
        self.formMain.show()
        Gtk.main()
        
    def onClose(self, *args):
        print "onClose"
        Gtk.main_quit(*args)
        exit()
    
    def onMenuitemNou(self, widget):
        self.formNew = self.builder.get_object("dialog1")
        self.formNew.set_transient_for(self.formMain)
        self.numQuestions = self.controller.getNumQuestions()
        self.builder.get_object("entry3").set_text("")
        self.builder.get_object("entry2").set_text("")        
        self.formNew.show()
        print "onMenuitemNouClick"
        
    def onMenuitemSurt(self, widget):
        print "onMenuitemSurtClick"
        Gtk.main_quit()
        exit()
        
    def onMenuitemAbout(self, widget):
        self.formAbout = self.builder.get_object("aboutdialog1")
        self.formAbout.set_transient_for(self.formMain)
        self.formAbout.run()
        self.formAbout.hide()
            
        print "onMenuitemAbout"
        
    def onButton1AcceptarClick(self, widget):
        self.name = self.builder.get_object("entry3").get_text()
        self.course = self.builder.get_object("entry2").get_text()
        self.formNew.hide()

        self.formQuestion = self.builder.get_object("dialog2")
        self.formQuestion.set_transient_for(self.formMain)
        self.setFormQuestion(self.numQuestion)
        self.formQuestion.show()        
        
        print "Nom: %s; Curs: %s" % (self.name, self.course)
        print "onButton1AcceptarClick"

    def setFormQuestion(self, numQuestion):
        [question, answers, self.rightAnswer] = self.controller.getQuestion(numQuestion)
        self.builder.get_object("label8").set_text("%s de %s curs" % (self.name, self.course))
        self.builder.get_object("label3").set_text("Pregunta %d" % numQuestion)
        self.builder.get_object("label4").set_text(question)
        self.builder.get_object("checkbutton1").set_label(answers[1])
        self.builder.get_object("checkbutton2").set_label(answers[2])
        self.builder.get_object("checkbutton3").set_label(answers[3])
        self.builder.get_object("checkbutton4").set_label(answers[4])
        self.builder.get_object("checkbutton1").set_active(False)
        self.builder.get_object("checkbutton2").set_active(False)
        self.builder.get_object("checkbutton3").set_active(False)
        self.builder.get_object("checkbutton4").set_active(False)

    def onButton2CancelarClick(self, widget):
        self.formNew.hide()
        print "onButton2CancelarClick"

    def onButtonSeguir(self, widget):
        # validate right answer. increment question counter,
        myAnswer = (1 * self.builder.get_object("checkbutton1").get_active()) + \
                   (2 * self.builder.get_object("checkbutton2").get_active()) + \
                   (4 * self.builder.get_object("checkbutton3").get_active()) + \
                   (8 * self.builder.get_object("checkbutton4").get_active())
        rightAnswer = pow(2, self.rightAnswer - 1)

        if myAnswer == rightAnswer:
            self.rightAnswersCounter = self.rightAnswersCounter + 1

        self.numQuestion = self.numQuestion + 1

        if self.numQuestion <= self.numQuestions:
            self.setFormQuestion(self.numQuestion)
            self.formQuestion.show()
        else:
            self.formQuestion.hide()
            self.formScoring = self.builder.get_object("dialog3")
            self.formScoring.set_transient_for(self.formMain)
            self.builder.get_object("labelNom").set_text("%s de %s curs" % (self.name, self.course))
            self.builder.get_object("label6").set_text("%d preguntes" % self.rightAnswersCounter)
            self.builder.get_object("label7").set_text("d'un total de %d preguntes" % self.numQuestions)
            self.formScoring.show()        

        print "onButtonSeguir"
        
    def onCancelarQuizz(self, widget):
        self.reset()
        self.formQuestion.hide()
        print "onCancelarClick"

    def onTancarClick(self, widget):
        self.reset()
        self.formScoring.hide()
        print "onTancarClick"

    def reset(self):
        self.name = ""
        self.course = ""
        self.numQuestion = 1
        self.rightAnswer = 0
        self.rightAnswersCounter = 0

Millores
El programa és una maqueta i es pot ampliar de moltes formes. Se m’acuden les següents millores:
– desar nom, curs i resultat obtingut a la BD.
– incorporar un cronòmetre per qüestionari: temps màxim de resolució del qüestionari.
– incorporar un cronòmetre per pregunta: temps màxim per pregunta.
– possibilitat de navegació endavant i endarrere pel qüestionari (ara només deixa avançar)
– possibilitat de nombre variable de possibles respostes
– possibilitat de vàries (o cap) respostes correctes possibles
– possibilitat de repetir el qüestionari un nombre màxim de cops.

Més millores:
– Afegir pantalles per a poder entrar nous qüestionaris
– perfils d’usuari (administrador, pot crear qüestionaris; usuari, pot resoldre qüestionaris)
– tenir en compte el curs i que l qüestionari s’adapti als nivell de l’usuari
– selecció de qüestionaris per tòpic, o per codi…
– descarregar d’Internet nous qüestionaris i desar-los en local per a execució off-line
– publicació a un servidor dels resultats obtinguts
– …

En fi. Un munt de possibilitats. De fet, si fem servir programari educatiu veurem un munt d’opcions que es podrien implementar a l’aplicació.

Repositori Github
Tot el codi el podeu trobar al github, a https://github.com/abaranguer/quiz

Un Curriculum fet amb l’org-mode de l’Emacs

Emacs és el veterà i llegendari editor de textos que va programar Richard Stallman al MIT allà pel 1975. Amb el pas dels anys Emacs ha esdevingut alguna cosa més que un simple editor de textos i se li han incorporat multitud d’opcions i facilitats de tota mena.

És una eina molt potent, versàtil i extensible: una aplicació amb vocació de sistema operatiu, de la que no cal sortir per  a poder fer, pràcticament, totes les tasques que habitualment realitza un desenvolupador.

La potència d’Emacs ve, principalment, de la seva capacitat per a personalitzar-lo i fer-lo créixer. Més que un editor de textos, Emacs es pot considerar com una plataforma de desenvolupament basada en el llenguatge Lisp (en el seu dialecte ELisp).

Lisp és un dels llenguatges informàtics més antics, però és plenament vigent. No només és el llenguatge de l’Emacs. Ës ben viu i, per exemple, compta amb una moderna versió, Clojure,  per a la Java Virtual Machine.

40 anys d’extensions per l’Emacs donen per molt i n’hi han algunes que són, senzillament, genials. Una d’aquestes extensions, genials, és l’Org-Mode.

L’Org-Mode és una extensió de l’Emacs que permet prendre notes i apunts, organitzar-los, definir tasques, prioritzar-les, controlar temps d’execució de les tasques, o implementar el popular mètode GTD de David Allen… Seria com una mena de súper-agenda, planificador, control de projectes… també permet exportar tota aquesta organització a LaTeX, a Pdf… o a formats més exòtics com el format FreeMind (i crear fàcilment mind maps). De fet, Org-Mode és tan versàtil , com l’Emacs, que es pot fer servir de moltes formes diferents i trobar-li usos variats.

Doncs bé, circumstàncies personals i el fet d’estar de vacances m’han portat a dedicar algunes hores a posar al dia el meu currículum, però he pensat que millor fer-ho de forma que em servís per practicar alguna cosa. La “cosa” que que he “practicat” és l’org-mode de l’Emacs.

La millor explicació de l’Org-Mode i de com fer-lo servir l’he trobat en aquest vídeo d’una conferència que va dir Carsten Dominik, l’autor de l’Org-Mode, a les Google Tech Talks de 2008.

El procediment que he seguit ha estat

1. Recuperar la versio desactualitzada del CV i desar-lo com  text.

2. He completat la informació que faltava.

3. Amb Emacs en Org-Mode, anar reestructurant les diferents parts del text i organitzant-les en apartats, subapartats… reordenant on considerés que fos necessari.

4. He afegit formats (itàliques, negretes, subratllats…), enllaços a Internet, una bonica foto… tot fent ús de l’etiquetat que ofereix l’Org-Mode per a l’efecte.

5. He revisat, i tornat a revisar, l’ortografia, la sintaxi… (i segur que se m’han colat errors).

6. He exportat als formats:

HTML:  amb org-export-as-html

TeX (LaTeX) i PDF: amb org-export-as-pdf

ASCII: amb org-export-as-ascii

FREEMIND: Amb org-export-as-freemind

Aleshores, fent us del FreeMind he fet un pas addicional d’exportació i he obtingut el meu currículum en versio Flash (Cal dir, però, que la versió flash ha de polir-se una mica, perquè els formats de l’Org-Mode no s’han traduït correctament.)

7. He actualitzat el curriculum a aquest mateix blog, i he posat la versió Html, el PDF  i la versió Flash a un espai de pàgines personals que tinc des de fa temps.

8. He penjat el projecte -realment crec que l’edició del CV amb Org-Mode i Emacs s’ha semblat molt més a un petit projecte que no pas una simple ediciṕ de text- al meu GitHub. Si teniu interès per veure com és el font d’un document en org-mode, reviseu el fitxer cv.org.

I ara una confessió: En la guerra religiosa entre Vimàires i Emacsers jo he estat, durant molts anys, essencialment agnòstic. Però d’un parell d’anys cap aquí que m’he decantat.

Vi (o Vim) és una eina extraordinària (amb el punt a favor que forma part de les instal·lacions base de les distribucions Linux, o sigui, que no cal instalar-lo) però, que els vimàires em perdonin, crec que avui ja no podria passar sense l’Emacs.

real_programmers

😉 Bones vacances!

ERO a Indra

És conegut al sector que Indra ha presentat un ERO que es preveu que afecti a 1850 treballadors de la companyia a tot l’estat (un 10% de la plantilla).

Jo sóc treballador de Indra. A aquestes alçades del procés ignoro si estic al grup dels treballadors que aniran al carrer o si conservaré el lloc de treball però les meves condicions laborals es veuran revisades. En tot cas, sigui d’un forma o de l’altre, estic afectat. Tots els companys de la plantilla ho estem.

L’empresa ha presentat una sèrie de motius econòmics per a l’ERO. Se’m fa molt difícil dir si aquests motius justifiquen l’acció de l’empresa però, com a mínim, em fa arrufar el nas. Personalment tinc la sospita que el motiu per a aquest ERO és, simplement, d’interessos particulars que aprofiten la situació política i, potser també, en previsió de canvis a la situació política.

En definitiva, la crisi va colpejar el sector de la consultoria i les TI, i ho va fer durament: per exemple Atos, T-Systems, Cap Gemini han patit EROs i modificacions a les condicions laborals. Alguna d’elles encara està patint ajustos, com Cap Gemini que està en un ERO actualment. El sector està convuls des de ben bé el 2011.

La consultoria va veure com la recessió del 2008 que va enviar milions de treballadors al carrer també arribava a les seves portes, però al sector ho va fer més tard, sobre el 2011. A Indra la crisi es va afrontar amb la congelació de sous i amb la reducció al mínim de nova contractació.

És dir, potser en el seu moment es va esquivar l’ERO però, tanmateix, les condicions laborals es van veure afectades i la mida de la plantilla s’ha mantingut essencialment estable des de fa anys, amb una progressiva baixada de la mitja dels sous per l’entrada de júniors substituint les baixes de perfils sèniors amb sous més alts.

És dir, Indra s’ha anat “adaptant” -i els treballadors ho hem patit- a la tendència del sector, però sense que calgués plantejar un ERO.

Doncs, finalment, quan algunes veus -optimistes? mal informades? mentideres?- van anunciant la “sortida de la crisi”, és quan es planteja un ERO a Indra.

Curiós, si més no. El meu nas segueix arrufat.

Biblioteques compartides a Linux, i com utilitzar-les des de Python amb ctypes

Shared libraries, Python, Linux… feia temps que tenia ganes de tractar aquestes qüestions i, finalment, he trobat el moment. El post d’avui plantejarà aquests punts:

1. Què és una static library de Linux?
2. Què és una shared library de Linux?
3. Com fer una shared library amb C?
4. Com fer servir la shared library?
5. Com invocar dinàmicament una shared library des de Python amb CTypes?
6. Com invocar dinàmicament una shared library des de C amb les funcions de càrrega dinàmica()?

1. Què són les static library
Per explicar què són les shared libraries és interessant explicar primer que són les static libraries. Reviseu Static Libraries, del Program Library HOWTO de The Linux Documentation Project (capítol 2 de PDF).

Les static librares són col·leccions de fitxers objecte ordinaris (fitxers “.o”, els que resulten de compilar amb gcc -c) que s’agrupen en biblioteques que acostumen a prendre l’extensió “.a”. Per a generar la biblioteca es fa servir el programa “ar” (“archiver”).

Les biblioteques estàtiques permeten enllaçar amb els programes sense que calgui compilar-ne el codi estalviant, doncs,  temps de recompilació; i, per tant, sense que calgui el codi de la biblioteca.

1.1. Com crear una static library
Per a crear una static library a partir dels fitxers objecte file1.o i file2.o la instrucció bàsica seria:

ar rcs biblioteca.a file1.o file2.o

les opcions rcs volen dir:
c: crea l’arxiu de biblioteca.
r: afegeix, o reemplaça, els fitxers objecte.
s: afegeix o actualitza l’índex a la biblioteca

Per a una referència completa, consulteu el manual:

man ar

1.2. Com fer servir l’static library
Un cop creada la biblioteca aleshores es fa servir al compilar amb l’opció gcc -lnom_biblioteca. Consulteu el manual

man gcc

Tingueu en compte les opcions:

-l library Cerca la biblioteca “library” durant l’enllaçat. Cal tenir en compte on s’escriu l’opció, l’enllaçador cerca i processa les biblioteques i els mòduls objecte en l’ordre en que apareixen a l’ordre de compilació; per tant, foo.o -lz bar.o cerca la llibreria z després de foo.o però abans de bar.o. Si bar.o fa referència a funcions de z, aquestes funcions no es carregaran.

Finalment, amb l’opció -L es pot indicar l’ubicació de la lliberia.

1.3. Nom de la static llibrary

L’enllaçador cerca la biblioteca per la llista de directoris estàndard. El nom del fitxer que cerca és “liblibrary.a”, és dir, prefixa el nom de la biblioteca amb “lib” i afegeix l’extensió “.a”. En l’exemple d’abans amb la biblioteca z l’enllaçador buscaria “libz.a”.

Atenció, doncs: seguint amb l’exemple anterior, a la creació de la biblioteca z amb ar faré servir el nom libz.a, però l’invocaré al gcc amb gcc -lz. z és el nom d’invocació, i libz.a el nom real de la biblioteca

1.4. Exemple

Amb un exemple es veurà tot més clar:

Creo una biblioteca estàtica amb els dos fitxers file1.c i file2.c (i file1.h, file2.h); les funcions de la biblioteca s’invoquen des d’una funció main definida al fitxer principal.c (i principal.h). Al Makefile es troben els detalls de l’utilització. Fixeu-vos que, com era d’esperar, el resultat de compilar fent servir la biblioteca, o fent servir els mòduls objecte directament és el mateix en ambdós casos. Fixem-nos també amb la mida dels binaris (biblioteca, mòduls objecte i executables) obtinguts.

file1.c

#include 

int funcio1(int num) {
  int i;

  for(i=1; i<=num; i++) {
    printf("fun1 - %d\n",i);
  }

  return 0;
}

file2.c

#include 

int funcio2(int num) {
  int i;  

  for(i=1; i<=num; i++) {
    printf("fun2 - %d\n",i);
  }

  return 0;
}

Els fitxers de capçalera, file1.h i file2.h

file1.h

/* function prototype */
extern int funcio1(int val);

file2.h

/* function prototype */
extern int funcio2(int val);

i el fitxer principal.c, que invoca les funcio1 i funcio2.

#include  

#include "file1.h"
#include "file2.h"
 
int main(int argc, char *argv[]) {
  funcio1(5);
  funcio2(8);

  return 0; 
} 

Makefile que genera la biblioteca estàtica i dos executables, el primer fent servir la biblioteca estàtica; i el segon, enllaçant directament els mòduls objecte

all: principal2 principal clean

file1.o: file1.c
    gcc -c file1.c

file2.o: file2.c
    gcc -c file2.c

proves: file1.o file2.o
    ar rcs libprova.a file1.o file2.o

principal.o: principal.c 
    gcc -c principal.c

principal: principal.o proves
    gcc -o principal principal.o -L. -lprova

principal2: principal.o file1.o file2.o
    gcc -o principal2 principal.o file1.o file2.o

Faré servir aquests fitxers de prova al llarg del post.

Si executo el makefile

albert@atenea:/media/albert/post/prova-lib$ make
gcc -c principal.c
gcc -c file1.c
gcc -c file2.c
gcc -o principal2 principal.o file1.o file2.o
ar rcs libprova.a file1.o file2.o
gcc -o principal principal.o -L. -lprova

Es genera la biblioteca estàtica i els executables principal i principal2 que, com era d’esperar, són de la mateixa mida.

aalbert@atenea:/media/albert/post/prova-lib$ ls -al
total 152
drwx------ 2 albert albert 8192 jun 13 21:42 .
drwx------ 4 albert albert 8192 jun 13 20:47 ..
-rw-r--r-- 1 albert albert  127 jun 13 21:08 file1.c
-rw-r--r-- 1 albert albert   54 jun  2 23:29 file1.h
-rw-r--r-- 1 albert albert 1052 jun 13 21:42 file1.o
-rw-r--r-- 1 albert albert  129 jun 13 21:08 file2.c
-rw-r--r-- 1 albert albert   54 jun  2 23:29 file2.h
-rw-r--r-- 1 albert albert 1052 jun 13 21:42 file2.o
-rw-r--r-- 1 albert albert 2320 jun 13 21:42 libprova.a
-rw-r--r-- 1 albert albert  519 jun 13 21:41 Makefile
-rw-r--r-- 1 albert albert 7394 jun 13 21:42 principal
-rw-r--r-- 1 albert albert 7394 jun 13 21:42 principal2
-rw-r--r-- 1 albert albert  140 jun  2 23:29 principal.c
-rw-r--r-- 1 albert albert  992 jun 13 21:42 principal.o

2. Què són les shared library?

Pe contestar aquesta pegunta, l’article de referència és Shared Libraries, del Program Library HOWTO de The Linux Documentation Project (capítol 3 de PDF).

Literalment, shared library vol dir biblioteca -de funcions- compartida. Les shared libraries són mòduls que s’enllacen en temps d’execució, en el moment de carregar-se l’aplicació que les enllaça, és dir en runtime; a diferència de les biblioteques estàtiques que s’enllacen en temps de compilació.

Això permet que les aplicacions que les invoquen tinguin executables més petits i que siguin més fàcils de mantenir i actualitzar.

A més, mitjançant la crida al sistema dlopen(), les shared libraries també poden carregar-se dinàmicament en qualsevol moment de l’execució i no només a l’inici. Per aquesta raó, les shared libraries també se les coneix com DLL, Dinamic Link Libraries, tot i que potser aquesta nomenclatura és més utilitzada a entorns Windows.

Que diferents tipus d’aplicacions puguin fer servir les mateixes shared libraries vol dir que les funcions i dades d’aquestes biblioteques no poden fer referencies a adreces de memòria absolutes, doncs l’enllaçat farà que les posicions de memòria en que finalment s’ubicaran puguin variar entre les diferents aplicacions. Això ens diu que per generar les shared libraries caldrà demanar que el mòdul objecte generat sigui reubicable.

Una shared library pot enllaçar-se simultàniament a diverses aplicacions. Quan la shared library està correctament instal·lada i respectant uns convenis de noms, el sistema de biblioteques compartides permet la convivència de diferents versions de la mateixa llibreria i el seu us per les diferents aplicacions.

2.1. Noms de les biblioteques compartides
2.1.1. soname
La clau del sistema de shared libraries està en els convenis de noms a seguir. Cal distingir entre el nom real de la shared library i el seu “soname”.
El “soname” d’una shared library es construeix amb el prefix “lib”, seguit del nom de la biblioteca, seguit del sufix “.so”, un punt, i un número de versió que s’ha d’augmentar cada cop que es canviï la interface de la biblioteca, és dir quan canvia el nombre de les funcions, o la signatura de les funcions.
El soname complet de la biblioteca inclou també el path fins a la biblioteca.
En general, el soname no és la llibreria propiament dita, si no un enllaç simbòlic al nom real de la biblioteca compartida

2.1.2. nom real (“real name”)
El fitxer amb el modul objecte reubicable rep el nom real de la biblioteca. El nom real es construeix afegint al soname un “minor number” i, opcionalment, un altre punt i el nombre de release. El minor number i el de release permeten conèixer exactament quina versió de la biblioteca està instal·lada i habiliten un control fi de la configuració. El nom complet de la biblioteca inclou també el path fins a la biblioteca.
El “real name” complet de la biblioteca inclou també el path fins a la biblioteca.

2.1.3. Nom per l’enllaçador (“linker name”)
És el nom que es fa servir per utilitzar la biblioteca. Es el soname sense número de versió. Com amb la resta de noms, el linker name complet de la biblioteca inclou també el path fins a la biblioteca.

2.1.4 Nom d’invocació

Com amb les biblioteques estàtiques, la invocació de la biblioteca al gcc amb l’opció -l es fa pel seu “nom d’invocació”, que serveix per construir el linker name. La cadena és, doncs, Nom d’invocació – Linker Name – Soname – Real name

2.2. utilització dels noms

La gestió de les biblioteques compartides és basa en la separació d’aquests noms.

– Utilització: quan les aplicacions llisten internament les shared libraries que necessiten només han de llistar el sonames requerits.

– Creació: quan es crea una shared library s’ha de crear amb tota la informació de versió completament detallada, és dir, els noms reals (“real names”).

– Instal·lació 1. ldconfig. La instal·lació d’una shared library s’ha de fer a alguna de les carpetes especials destinades a acollir biblioteques compartides i aleshores executar el programa ldconfig. ldconfig examina els fitxers existents i crea els sonames, és dir, els enllaços simbòlics als real names. A més actualitza la caché de biblioteques compartides /etc/ld.so.cache.

– Instal·lació 2. linker name. ldconfig no estableix els “linker names”, això es fa durant la instal·lació de la biblioteca, amb un script d’instal·lació, en general. El linker name, en general, és un enllaç simbòlic al soname més recent.

Aquest joc d’enllaços simbòlics permet que es pugui apuntar a una versió o una altre de les biblioteques. Per exemple, en desenvolupament, amb finalitats de depuració enllaçant diferents versions de la biblioteca.

Per exemple, la shared library que implementa readline:

/usr/lib/libreadline.so és el linker name complet i és l’enllaç simbòlic a…
/usr/lib/libreadline.so.3 és el soname complet i és l’enllaç simbòlic a…
/usr/lib/libreadline.so.3.0 que és el nom real complet i el mòdul objecte que conté el codi.

Els tres noms anteriors, en principi, són necessaris en runtime.

En compilació, a més, es fa servir el nom d’invocació de la biblioteca a l’opció -l del gcc: és dir,  gcc -lreadline

3. Com crear una Shared Library?

És molt semblant al procés de crear una Static Library.
Primer: crear els diferents mòduls objecte que conformen la biblioteca, amb l’opció -fPIC que genera codi reubicable.
Segon: amb els mòduls objecte creats, ja es pot construir la biblioteca amb gcc:

gcc -shared -Wl,-soname,soname_de_la_library -o real_name_library mòdul1.o mòdul2.o... -lc

El següent Makefile mostra el procediment:

all: prova

file1.o: file1.c
    gcc -fPIC -g -c -Wall file1.c

file2.o: file2.c
    gcc -fPIC -g -c -Wall file2.c

prova: file1.o file2.o
    gcc -shared -Wl,-soname,libprova.so.1 -o libprova.so.1.0.1 file1.o file2.o -lc

Com a remarques: a la generació dels mòduls objecte he fet servir,a més de -fPIC que ja he explicat, les opcions -g, per afegir informació de depuració i -Wall, per a mostrar tots els missatges de warnings. Són opcions que faciliten el desenvolupament.

La línia que construeix la shared library inclou

-shared indicar que és una shared library
-Wl,-soname,libprova.so.1 -Wl passa paràmetres a l’enllaçador. El paràmetre passat és el soname. Cal posar les comes i no es poden deixar espais en blanc que no estiguin “escapats”
-o libprova.so.1.0.1 és el “real name” de la biblioteca
-lc enllaça amb la lliberia libc.a estàtica

Aleshores, executant make obtinc la shared library libprova.so.1.0.1

albert@atenea:/media/albert/16GB/post shared libraries/prova-lib$ make
gcc -fPIC -g -c -Wall file1.c
gcc -fPIC -g -c -Wall file2.c
gcc -shared -Wl,-soname,libprova.so.1 -o libprova.so.1.0.1 file1.o file2.o -lc

i , efectivament:

albert@atenea:/media/albert/post/prova-lib$ ls -al
total 168
drwx------ 2 albert albert 8192 jun 13 22:22 .
drwx------ 4 albert albert 8192 jun 13 20:47 ..
-rw-r--r-- 1 albert albert  127 jun 13 21:08 file1.c
-rw-r--r-- 1 albert albert   54 jun  2 23:29 file1.h
-rw-r--r-- 1 albert albert 2660 jun 13 22:22 file1.o
-rw-r--r-- 1 albert albert  129 jun 13 21:08 file2.c
-rw-r--r-- 1 albert albert   54 jun  2 23:29 file2.h
-rw-r--r-- 1 albert albert 2660 jun 13 22:22 file2.o
-rw-r--r-- 1 albert albert 8271 jun 13 22:22 libprova.so.1.0.1
-rw-r--r-- 1 albert albert  247 jun 13 22:22 Makefile
-rw-r--r-- 1 albert albert  140 jun  2 23:29 principal.c

4. Fer servir la biblioteca compartida

El següent pas és fer servir la biblioteca. Hi han dos punts de vista a tenir en compte:

4.1 El desenvolupador

El desenvolupador genera versions de la biblioteca i requereix provar-les ràpidament. Per al desenvolupador la ubicació de la biblioteca ha de poder ser qualsevol lloc que li vagi bé.

Hi han dues opcions principals:

– o bé fer servir LD_LIBRARY_PATH a la sessió actual

– o bé instal·lar de forma permanent la biblioteca a una carpeta de desenvolupament

4.1.1 Per sessió, amb LD_LIBRARY_PATH

Al punt 3 he creat la  biblioteca libprova.so.1.0.1. Ara la faig servir per crear un executable amb el següent makefile

all: principal3

principal.o: principal.c
    gcc -o principal.o -c principal.c

principal3: principal.o
    gcc -o principal3  principal.o -lprova -L.

El resultat de la compilació és l’executable principal3

albert@atenea:~/workspace/prova-lib$ make
gcc -o principal3  principal.o -lprova -L.
albert@atenea:~/workspace/prova-lib$ ls -al
total 100
drwxrwxr-x  2 albert albert 4096 jun 13 23:19 .
drwxrwxr-x 13 albert albert 4096 jun  6 21:46 ..
-rw-r--r--  1 albert albert  127 jun 13 21:08 file1.c
-rw-r--r--  1 albert albert   54 jun  2 23:29 file1.h
-rw-r--r--  1 albert albert  129 jun 13 21:08 file2.c
-rw-r--r--  1 albert albert   54 jun  2 23:29 file2.h
-rw-r--r--  1 albert albert 8271 jun 13 22:22 libprova.so.1.0.1
-rw-r--r--  1 albert albert  185 jun 13 22:54 Makefile
-rw-r--r--  1 albert albert 7394 jun 13 21:42 principal
-rw-r--r--  1 albert albert 7394 jun 13 21:42 principal2
-rwxrwxr-x  1 albert albert 7316 jun 13 23:19 principal3
-rw-r--r--  1 albert albert  140 jun  2 23:29 principal.c
-rw-rw-r--  1 albert albert  992 jun 13 23:04 principal.o

Es pot veure que ha generat principal3 i que, efectivament és més petit que executables obtinguts amb les biblioteques estàtiques, fins aquí bé. Però què passa quan provo d’executar principal3?

albert@atenea:~/workspace/prova-lib$ principal3
principal3: error while loading shared libraries: libprova.so.1: cannot open shared object file: No such file or directory

Que no troba la biblioteca. Com he dit abans, les biblioteques compartides han d’estar a unes carpetes específiques. O bé, a les carpeta indicades a LD_LIBRARY_PATH (cal, però, anar amb compte amb LD_LIBRARY_PATH).

Aquest problema es veu molt clar amb ldd (Consulteu el manual man ldd) que torna  les biblioteques compartides que fa servir un executable.

albert@atenea:~/workspace/prova-lib$ ldd principal3
    linux-gate.so.1 =>  (0xb7798000)
    libprova.so.1 => not found
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75cc000)
    /lib/ld-linux.so.2 (0xb7799000)

Com he explicat abans, el procediment és executar ldconfig i crear un enllaç simbòlic del linker name a soname.  Som-hi:

albert@atenea:~/workspace/prova-lib$ ldconfig -n .
albert@atenea:~/workspace/prova-lib$ ln -s ./libprova.so.1 ./libprova.so
albert@atenea:~/workspace/prova-lib$ ls -al
total 100
drwxrwxr-x  2 albert albert 4096 jun 13 23:34 .
drwxrwxr-x 13 albert albert 4096 jun  6 21:46 ..
-rw-r--r--  1 albert albert  127 jun 13 21:08 file1.c
-rw-r--r--  1 albert albert   54 jun  2 23:29 file1.h
-rw-r--r--  1 albert albert  129 jun 13 21:08 file2.c
-rw-r--r--  1 albert albert   54 jun  2 23:29 file2.h
lrwxrwxrwx  1 albert albert   15 jun 13 23:34 libprova.so -> ./libprova.so.1
lrwxrwxrwx  1 albert albert   17 jun 13 23:34 libprova.so.1 -> libprova.so.1.0.1
-rw-r--r--  1 albert albert 8271 jun 13 22:22 libprova.so.1.0.1
-rw-r--r--  1 albert albert  185 jun 13 22:54 Makefile
-rw-r--r--  1 albert albert 7394 jun 13 21:42 principal
-rw-r--r--  1 albert albert 7394 jun 13 21:42 principal2
-rwxrwxr-x  1 albert albert 7316 jun 13 23:19 principal3
-rw-r--r--  1 albert albert  140 jun  2 23:29 principal.c

“ldconfig n .” busca sobre el directori actual i crea l’enllaç simbòlic al soname

libprova.so.1 -> libprova.so.1.0.1

Manualment, amb ln -s he creat l’enllaç simbòlic al linker name

libprova.so -> ./libprova.so.1

I ara, ja només cal establir amb  LD_LIBRARY_PATH  que a la carpeta /home/abert/workspace/prova-lib hi han biblioteques compartides

albert@atenea:~/workspace/prova-lib$ LD_LIBRARY_PATH=/home/albert/workspace/prova-lib/
albert@atenea:~/workspace/prova-lib$ export LD_LIBRARY_PATH

i, per descomptat, ara sí que funciona:   😉

albert@atenea:~/workspace/prova-lib$ principal3
fun1 - 1
fun1 - 2
fun1 - 3
fun1 - 4
fun1 - 5
fun2 - 1
fun2 - 2
fun2 - 3
fun2 - 4
fun2 - 5
fun2 - 6
fun2 - 7
fun2 - 8
albert@atenea:~/workspace/prova-lib$

El procediment amb LD_LIBRARY_PATH només hauria de fer-se servir en desenvolupament, de forma puntual per provar.

4.1.1 Permanent, a una carpeta especificada a /etc/ld.so.conf

Si torno LD_LIBRARY_PATH al seu estat original principal3 deixa de funcionar. Per exemple, si apago l’ordinador i el torno engegar, LD_LIBRARY_PATH perd els canvis. En lloc d’establir de forma general un valor per LD_LIBRARY_PATH, que pot ser perillós, una alternativa segura és especificar una carpeta pròpia de desenvolupament a /etc/ld.so.conf

Resetejo LD_LIBRARY_PATH, i preparo la carpeta de desenvolupament, copiant-hi la biblioteca compartida, executant ldconfig i establint l’enllaç simbòlic del linker name

albert@atenea:~/workspace/prova-lib$ LD_LIBRARY_PATH=
albert@atenea:~/workspace/prova-lib$ export LD_LIBRARY_PATH
albert@atenea:~/workspace/prova-lib$ mkdir /home/albert/dev-lib
albert@atenea:~/workspace/prova-lib$ cp libprova.so.1.0.1 /home/albert/dev-lib/
albert@atenea:~/workspace/prova-lib$ cd /home/albert/dev-lib/
albert@atenea:~/dev-lib$ ldconfig -n /home/albert/dev-lib/
albert@atenea:~/dev-lib$ ln -s /home/albert/dev-lib/libprova.so.1 /home/albert/dev-lib/libprova.so
albert@atenea:~/dev-lib$ ls -al
total 20
drwxrwxr-x  2 albert albert 4096 jun 14 00:03 .
drwxr-xr-x 80 albert albert 4096 jun 14 00:01 ..
lrwxrwxrwx  1 albert albert   34 jun 14 00:03 libprova.so -> /home/albert/dev-lib/libprova.so.1
lrwxrwxrwx  1 albert albert   17 jun 14 00:02 libprova.so.1 -> libprova.so.1.0.1
-rw-r--r--  1 albert albert 8271 jun 14 00:01 libprova.so.1.0.1
albert@atenea:~/dev-lib$

En aquest moment, principal3 falla perquè no troba la biblioteca compartida.

Ara estableixo a /etc/ld.so.conf la nova carpeta on cercar biblioteques compartides

sudo emacs /etc/ld.so.conf

/etc/ld.so.conf queda així

include /etc/ld.so.conf.d/*.conf
# afegeixo carpeta de biblioteques de desenvolupament
/home/albert/dev-lib

A continuació cal indicar que es tingui en compte la nova carpeta i es reconstrueixi la memòria cau de biblioteques compartides. Es fa amb ldconfig:

sudo ldconfig

En aquest moment, principal3 torna a ser operativa. Si ara examino la memòria cau de biblioteques compartides observo que, efectivament, està fent servir la biblioteca de /home/albert/dev-lib

albert@atenea:~/workspace/prova-lib$ principal3 
fun1 - 1
fun1 - 2
fun1 - 3
fun1 - 4
fun1 - 5
fun2 - 1
fun2 - 2
fun2 - 3
fun2 - 4
fun2 - 5
fun2 - 6
fun2 - 7
fun2 - 8
albert@atenea:~/workspace/prova-lib$ ldconfig -p | grep "prova"
    libprova.so.1 (libc6) => /home/albert/dev-lib/libprova.so.1
    libprova.so (libc6) => /home/albert/dev-lib/libprova.so
albert@atenea:~/workspace/prova-lib$

4.2 El distribuïdor

L’altre punt de vista d’ús de la biblioteca és el del distribuïdor de la biblioteca. Hi han un parell de criteris a tenir en compte en la distribució de codi i en la ubicació de les biblioteques compartides.

Ambdós criteris estableixen ubicacions permanents de les biblioteques compartides i, per tant, es defineixen a /etc/ld.so.conf.
4.2.1 Distribució seguint l’Estàndard GNU
L’estàndard aplica a projectes de desenvolupament de codi obert. Seguint aquest estàndard, les biblioteques han d’anar a /usr/local/lib, i els executables a /usr/local/bin
4.2.2 Filesystem Hierarchy Standard (FHS)
L’estàndard aplica al programari d’una distribució. La majoria de les biblioteques ha d’anar a /usr/lib, i les biblioteques per a l’arrencada del sistema, a /lib

En general, quan es distribueix codi de projectes GNU, és dir, projectes en evolució i construcció, la recomanació és seguir el GNU Standard. En canvi, quan el codi està en “producció” i s’inclou a una distribució de Linux, aleshores la recomanació és ubicar les biblioteques d’acord amb el criteri FHS.

5. Com invocar una shared library des de Python amb CTypes?

No me n’he oblidat. Per acabar, Python incorpora el package CTypes (documentació) que li permet enllaçar shared libraries i fer-ne us de les seves funcions de forma molt senzilla, semblant al LoadLibrary de VisualBasic.

És directe:
– faig l’import;
– amb CDLL carrego la biblioteca i obtinc un handle per fer-la servir. CDLL rep el soname com a paràmetre.
– Faig servir el handle per invocar les funcions de la biblioteca.

albert@atenea:~/workspace/prova-lib$ python
Python 2.7.6 (default, Mar 22 2014, 22:59:38) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import *
>>> prova = CDLL("libprova.so.1")
>>> ret = prova.funcio1(7)
fun1 - 1
fun1 - 2
fun1 - 3
fun1 - 4
fun1 - 5
fun1 - 6
fun1 - 7
>>> ret = prova.funcio2(3)
fun2 - 1
fun2 - 2
fun2 - 3

6. Com invocar una shared library des de C amb les funcions de càrrega dinàmica()?

El que he fet al punt 4 és una invocació dinàmica de la shared library prova des de Python. Això mateix també es pot fer amb C, fent servir les crides al sistema dl*, (dlopen(), dlsym(), dlerror() i dlclose()). La referència, en aquest cas, Dynamically Loaded (DL) Libraries, del Program Library HOWTO de The Linux Documentation Project (capítol 4 del PDF).

També és directe. Per il·lustrar el procediment, faré un principal4 que, com es veu al makefile, no invoca la biblioteca prova, sino la dl (Dynamic Load), que és la que aporta les funcions de càrrega dinàmica:

Makefile

all: principal4

principal_dl.o: principal_dl.c
    gcc -o principal_dl.o -c principal_dl.c

principal4: principal_dl.o
    gcc -o principal4  principal_dl.o -ldl

principal_dl.c

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char **argv) {
    void *handle;
    int (*fun1)(int);   /* extern int funcio1(int val); */
    int (*fun2)(int);   /* extern int funcio2(int val); */
    char *error;
    int ret1;
    int ret2;
    
    handle = dlopen ("libprova.so.1", RTLD_LAZY);
    if (!handle) {
        fputs (dlerror(), stderr);
        exit(1);
    }
    
    fun1 = dlsym(handle, "funcio1");
    if ((error = dlerror()) != NULL)  {
        fputs(error, stderr);
        exit(1);
    }

    fun2 = dlsym(handle, "funcio2");
    if ((error = dlerror()) != NULL)  {
        fputs(error, stderr);
        exit(1);
    }

    printf("Sortida de funcio1(3)\n");
    ret1 = (*fun1)(3);
    printf("\nSortida de funcio2(7)\n");
    ret2 = (*fun2)(7);
    printf("\nret1: %d; ret2: %d\n\n", ret1, ret2);

    dlclose(handle);
}

Del codi anterior, destacar que:

– He definit fun1 i fun2 com punters a funcions amb la  mateixa signatura de funcio1 i funcio2;

– La utilització de dlopen() per carregar dinàmicament la biblioteca, passant el soname com a paràmetre. dlopen() retorna un “handle”, una referència, a la biblioteca.

– Amb aquest handle, faig servir dlsym() per trobar les funcions a executar. dlsym() retorna punters a les funcions

– Faig servir els punters a les funcions, és dir, utiloitzo les funcions de la biblioteca.

– Al llarg de tot el programa, si hi ha algun error de càrrega de la biblioteca, aquest es pot obtenir amb dlerror()

– Quan he fet servir la biblioteca, la descarrego amb dlclose()

7. Resum
En aquest post he explicat com
– Què és una static library.
– Què és una shared library.
– Com fer una shared library amb C.
– Com fer servir la shared library.
– Com invocar una shared library des de Python amb ctypes.
– I com invocar una shared library des de C amb el grup de funcions de càrrega dinàmica().

EL codi del post es pot descarregar del  repositori GitHub.

Segueix

Get every new post delivered to your Inbox.