Sockets, Timers i GUI amb Python i Tkinter

No me n’he pogut estar i, després d’haver implementat el petit servidor amb Java i C en les versions de text, swing (java) i GTK (C), het fet la implementació amb Python i Tkinter.

Per a muntar la GUI hagués pogut partit de la GUI ja generada amb Glade per al versió en C (el post anterior), però he preferit fer servir el Tkinter, atès que forma part dels mòduls estàndard que es distribueixen amb Python, sense que calgui cap instal·lació addicional.

Vet aquí la interfície que he generat

És dir, un widget text i tres botons: Clear, per netejar el widget; Quit, per tancar l’aplicació i Add per afegir un text de prova.

Internament l’aplicació crea un parell de classes : Una classe ServerSocket (línies 71 a 104) que es deriva de threading.Thread i que és el servidor pròpiament dit.

El mètode run (línies 78 a 85), accepta les connexions entrants i crea (accept) un clientsocket (línia 86). Envia (send) un missatge de benvinguda (línies 87 a 91) i passa a escoltar els missatges (recv) dels clients (entre 92 i 104).

Per a cada missatge rebut, el neteja d’espais en blanc i el posa a la cua de la Gui per a la seva visualització (línia 101) . Aquest procés es repeteix fins que rep un “END” (línia 99).

La classe AppGui (línies 10 a 69) que s’encarrega de la interfície d’usuari i, particularment, de mostrar els missatges que escolta el servidor. Essencialment tota la classe es dedica a la creació de la GUI i la connexió d’events amb els seus mètodes handler respectius.

Destacar el mètode update_gui (entre 45 i 59) que es llença dins del fil de dispatcher d’events del Tkinter amb el mètode after (línia 43) amb un delay inicial de 100 milisegons.

El mètode update no fa més que una lectura no bloquejant (get_nowait) a la línia 48. Si no hi ha res per llegir, llença una excepció. Aquesta excepció és ignorada.

En cas de no llençar excepció, vol dir que s’ha llegit alguna cosa. El que s’ha llegit es mostra al widget text (línia 54) . Si n o s’ha llegit res, o el que s’ha llegit no és “END” es rearma el timer. (línies 58 i 59)

La cua és una instància de la classe Queue del mòdul queue. El primer que es fa al bloc main (106 a 112) és crear una instància de la cua i posar-la a disposició del server (108), s’activa el fil del server (109) i es llença la GUI (110).

Vet aquí el codi :

import tkinter as tk
import tkinter.messagebox as msgbox
import tkinter.scrolledtext as scrolledtext
import socket
from datetime import datetime
import threading
import queue
import sys

class AppGui:
    wnd = None
    button_clear = None
    button_quit = None
    button_add = None
    text_field = None
    queue_channel = None
    i = 0

    def clear_onclick(self):
        self.text_field.delete('1.0', 'end')

    def quit_onclick(self):
        sortir = msgbox.askokcancel("Confirmació", "prem OK per sortir")
        if sortir:
            self.wnd.destroy()

    def add_onclick(self):
        self.text_field.insert(tk.CURRENT, 'Això és un text de prova\n')

    def create_widgets(self, wnd):
        self.frame = tk.Frame(self.wnd)

        self.button_clear = tk.Button(self.frame, text="Clear", command=self.clear_onclick)
        self.button_quit = tk.Button(self.frame, text="Quit", command=self.quit_onclick)
        self.button_add = tk.Button(self.frame, text="Add", command=self.add_onclick)
        self.text_field = scrolledtext.ScrolledText(self.frame, height=32, width=80)
        self.button_clear.grid(row=1, column=0, padx=5, pady=5)
        self.button_quit.grid(row=1, column=1, padx=5, pady=5)
        self.button_add.grid(row=1, column=2, padx=5, pady=5)
        self.text_field.grid(row=0, column=0, padx=5, pady=5, columnspan=3)
        self.frame.grid()

        self.wnd.after(100, self.update_gui)

    def update_gui(self):
        chunk = ""
        try:
            chunk = self.queue_channel.get_nowait()
        except queue.Empty:
            pass
        else:
            if len(chunk) > 0:
                print("chunk ({0}) : {1}".format(self.i, chunk))
                self.text_field.insert(tk.CURRENT, chunk + '\n')

        self.i = self.i + 1

        if chunk != 'END':
            self.wnd.after(100, self.update_gui)

    def __init__(self, queue_channel):
        self.queue_channel = queue_channel

        self.wnd = tk.Tk()
        self.wnd.title("Lab Threads Server Socket")
        self.wnd.resizable(False, False)
        self.create_widgets(self.wnd)

        self.wnd.mainloop()

class ServerSocket(threading.Thread):
    queue_channel = None

    def __init__(self, queue_channel):
        threading.Thread.__init__(self)
        self.queue_channel = queue_channel

    def run(self):
        print("server socket")
        serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        print("bind")
        serversocket.bind((socket.gethostname(), 8085))
        print("listen")
        serversocket.listen(5)
        print("accept")
        (clientsocket, address) = serversocket.accept()
        now = datetime.now()
        date_time = now.strftime("%d/%m/%Y, %H:%M:%S")
        message = "Hello from Python Server : {0}\n".format(date_time)
        print(message)
        clientsocket.send(bytearray(message, 'utf-8'))
        serving = True
        BUFFER_SIZE = 1024
        i = 0
        while serving:
            data_bytes = clientsocket.recv(BUFFER_SIZE)
            data_string = data_bytes.decode("utf-8")
            data_trimmed = data_string.strip()
            if data_trimmed == 'END':
                serving = False
            queue_channel.put("Msg ({0}) : {1}".format(i, data_trimmed))
            i = i + 1

        print("End serving")

if __name__ == '__main__':
    queue_channel = queue.Queue()
    server = ServerSocket(queue_channel)
    server.start()
    client = AppGui(queue_channel)
    server.join()
    sys.exit(0)

Per a provar no hi ha més que obrir un parell de terminals, un per a llençar l’execució del programa i una altre amb nc artemis 8085 (artemis és el bonic nom del meu ordinador de sobretaula).

Vet aquí la consola amb el llençament de l’aplicació i el regitsre que va deixant

albert@artemis:~/workspace/PycharmProjects/lab_threads_gui$ python main.py
server socket
bind
listen
accept
Hello from Python Server : 08/08/2020, 19:39:29
chunk (45593) : Msg (0) : Hola,
chunk (45657) : Msg (1) : Això és una prova
chunk (45805) : Msg (2) : En aquesta ocasió amb la versió Python/Tkinte del servidor
chunk (46029) : Msg (3) : Funciona
chunk (46031) : Msg (4) :
chunk (46121) : Msg (5) : Si més no, fa l'esperat
chunk (46186) : Msg (6) : Per ara, acabo
End serving
chunk (46205) : Msg (7) : END

L’anterior és el resultat de la següent interacció amb nc artemis 8085

albert@artemis:~$ nc artemis 8085
Hello from Python Server : 08/08/2020, 19:39:29
Hola,
Això és una prova
En aquesta ocasió amb la versió Python/Tkinte del servidor
Funciona
Si més no, fa l'esperat
Per ara, acabo
END
albert@artemis:~$

I tot plegat, es veu així a la GUI :

Sockets, Timers i GUI amb C i GTK

Amb aquest post tanco la sèrie dedicada a l’aplicació servidora. En aquest post afegiré una interfície gràfica GTK al servidor en mode text del post anterior. per a realitzar la interfície gràfica faré servir Glade i Eclipse CDT, amb C com a llenguatge de programació.

GUI amb Glade

Glade és una eina que permet crear interfícies gràfiques amb GTK molt fàcilment, que és agnòstica pel que fa al llenguatge de programació a utilitzar i que està disponible tant a Linux com a Windows. L’eina genera un XML que és llegit per l’aplicació i a partir del qual es construeix la GUI. Això vol dir que cal desplegar tots dos, el binari de l’aplicació i l’xml amb la configuració de la GUI. També existeix la possibilitat d’encastar l’xml com un string dins l’aplicació (i construir la GUI amb gtk_builder_add_from_string()) si es vol evitar haver de distribuir l’XML.

La interfície que construeixo és la següent.

Es tracta de :

  1. Sobre una finestra d’aplicació
  2. amb GtkLayout
  3. afegir una scroll window…
  4. en la que hi poso un text view.
  5. i assigno un text buffer a aquest text view
  6. Finalment, tres botons : Clear per esborrar la text view ; Quit, per abandonar l’aplicació ; Add per afegir una línia de text al text view. Add és un botó que té a veure amb les proves que he anat fent i m’ha semblat d’interès mantenir.

Cal fer els ajustos necessaris per a que les mides de tot plegat quedin ben quadrades i, molt important també, cal indicar que els botons tenen els següents handlers associats a l’esdeveniment clicked

  • botó Clear, button_clear_clicked_cb
  • botó Quit. button_quit_clicked_cb
  • botó Add, button_add_clicked_cb

La interfície gràfica es desa com un fitxer xml que anomeno gui_lab_thread.glade, i que desaré un una carpeta resources

Vet aquí el fitxer .glade

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface domain="">
  <requires lib="gtk+" version="3.10"/>
  <object class="GtkTextBuffer" id="text_buffer"/>
  <object class="GtkWindow" id="app_window">
    <property name="width_request">640</property>
    <property name="height_request">480</property>
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">LAB Threads</property>
    <property name="resizable">False</property>
    <child>
      <object class="GtkLayout">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <child>
          <object class="GtkButton" id="button_clear">
            <property name="label" translatable="yes">Clear</property>
            <property name="width_request">100</property>
            <property name="height_request">40</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <property name="relief">half</property>
            <signal name="clicked" handler="button_clear_clicked_cb" swapped="no"/>
          </object>
          <packing>
            <property name="x">20</property>
            <property name="y">420</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="button_quit">
            <property name="label" translatable="yes">Quit</property>
            <property name="width_request">100</property>
            <property name="height_request">40</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="button_quit_clicked_cb" swapped="no"/>
          </object>
          <packing>
            <property name="x">140</property>
            <property name="y">420</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="button_add">
            <property name="label" translatable="yes">Add</property>
            <property name="width_request">100</property>
            <property name="height_request">40</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="button_add_clicked_cb" swapped="no"/>
          </object>
          <packing>
            <property name="x">260</property>
            <property name="y">420</property>
          </packing>
        </child>
        <child>
          <object class="GtkScrolledWindow" id="scrolled_window">
            <property name="width_request">600</property>
            <property name="height_request">380</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="shadow_type">in</property>
            <child>
              <object class="GtkTextView" id="text_view">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="buffer">text_buffer</property>
              </object>
            </child>
          </object>
          <packing>
            <property name="x">20</property>
            <property name="y">20</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

Eclipse i GTK

Per a poder compilar GTK amb Eclipse CDT cal certa configuració. Parteixo d’un projecte java normal al que hauré d’afegir la configuració necessària per a compilar aplicacions GTK.

En particular, cal indicar al compilador els includes i els lib que ha de fer servir. Per a determinar quins includes afegir a la compilació i quins libs al linkatge es fa servir l’eina pks-config, com s’indica a l’aplicació d’ajuda Devhelp, a la secció « Compiling GTK+ Applications on UNIX ».

Amb pks-config –cflags gtk+-3.0 obtinc els includes a afegir, a més del flag -pthread.

pkg-configalbert@artemis:~$ pkg-config --cflags gtk+-3.0
-pthread -I/usr/include/gtk-3.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -I/usr/include/gtk-3.0 -I/usr/include/gio-unix-2.0/ -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/pango-1.0 -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libpng16 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include
albert@artemis:~$

Amb pks-config –libs gtk+-3.0 obtinc les llibreries

albert@artemis:~$ pkg-config --libs gtk+-3.0
-lgtk-3 -lgdk-3 -lpangocairo-1.0 -lpango-1.0 -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0

La recomanació que fan als fòrums d’Eclipse (https://www.eclipse.org/forums/index.php/t/1071791/) és prou bona : crear un un parell de variables GTKINCS i GTKLIBS, per a fer-les servir a la configuració del projecte. A més també caldrà explicitar manualment els includes i les llibreries. A més, cal insistir en executar pks-config per obtenir la configuració correcta per al sistema en que s’està desenvolupant. Si simplement es copia el que es troba a Intenet, hi ha la possibilitat que el propi sistema no tingui la mateixa configuració i la compilació falli perquè no es troben includes o llibreries.

Vet aquí la configuració al meu Eclipse. Faig click a Project – Properties

I obro la pantalla de propietats. Trio « Totes les configuracions » i a « build variables » defineixo les variables :

A « Settings » – « GCC Compiler » faig servir la variable GTKINCS al «command line pattern»

A més, he d’indicar a l’Eclipse els dirs del includes

També indico que faré servir pthreads (a Miscellaneous, support for pthread).

Amb els libs he de fer el mateix que amb els includes. Primer afegeixo la variable a les opcions del linkador.

Si calgués afegir llibreries a l’Eclipse, ho faria aquí :

Arribats a aquest punt, afegeixo els fonts al projecte d’Eclipse i ja els podré compilar i generar l’executable.

El codi en C de la GUI ServerSocket

Vet aquí el codi. Els comentaris venen després.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <ctype.h>
#include <gtk-3.0/gtk/gtk.h>

#define SUCCESS 0
#define FAILURE 1
#define PIPE_FAILURE -1
#define SOCKET_FAILURE -1
#define ACCEPT_FAILURE -1
#define LISTEN_FAILURE -1
#define BIND_FAILURE -1
#define SEND_FAILURE -1
#define RECV_FAILURE -1
#define WRITE_ERROR -1
#define READ_ERROR -1
#define BUFFER_SIZE 1024
#define BUFFER_DATE_SIZE 80
#define PORT 8080

void button_quit_clicked_cb (GtkWidget *widget, gpointer data);
void button_clear_clicked_cb (GtkWidget *widget, gpointer data);
void button_add_clicked_cb (GtkWidget *widget, gpointer data);
char *ltrim(char *s);
char *rtrim(char *s);
char *trim(char *s);
gint non_blocking_pipe_read(gpointer data);
void init_widgets(int argc, char *argv[]);
void *secondary_thread(void *arg);
void on_window_main_destroy();

GtkWidget *win;
GtkWidget *btn_quit;
GtkWidget *btn_clear;
GtkWidget *btn_add;
GtkTextView *text_view;
GtkTextBuffer *text_buffer;
GtkTextIter iter;

char buffer[BUFFER_SIZE];
char buffer_read[BUFFER_SIZE];

int fd[2];
gint idTimeoutPipeNonBlockigRead;

void button_quit_clicked_cb (GtkWidget *widget, gpointer data) {
    gtk_main_quit ();
}

void button_clear_clicked_cb (GtkWidget *widget, gpointer data) {
	gtk_text_buffer_set_text (text_buffer, "", -1);
	gtk_text_buffer_get_start_iter(text_buffer, &iter);
}

void button_add_clicked_cb (GtkWidget *widget, gpointer data) {
	gtk_text_buffer_insert(text_buffer, &iter, "Afegeix una línia\n", -1);
}

char *ltrim(char *s) {
    while(isspace(*s)) s++;
    return s;
}

char *rtrim(char *s) {
    char* back = s + strlen(s);
    while(isspace(*--back));
    *(back+1) = '\0';
    return s;
}

char *trim(char *s) {
    return rtrim(ltrim(s));
}

gint non_blocking_pipe_read(gpointer data) {
	gint retValue;
	int num_bytes;
	char *trimmed;

	memset(buffer_read, 0, BUFFER_SIZE);
    num_bytes = read(fd[0], buffer_read, BUFFER_SIZE);

	retValue = TRUE;
	if (num_bytes > 0) {
		trimmed = trim(buffer_read);

        printf("data : %s\n", buffer_read);
        gtk_text_buffer_insert(text_buffer, &iter, buffer_read, -1);
        gtk_text_buffer_insert(text_buffer, &iter, "\n", -1);

		if (strcmp(trimmed, "END") == 0) {
			close(fd[0]);
			retValue = FALSE;
		}
    }

	return retValue;
}

void init_widgets(int argc, char *argv[]) {

	gtk_init(&argc, &argv);

	GtkBuilder *builder = gtk_builder_new();
	gtk_builder_add_from_file(builder, "./resources/gui_lab_thread.glade", NULL);

	win = GTK_WIDGET(gtk_builder_get_object(builder, "app_window"));
	btn_quit = GTK_WIDGET(gtk_builder_get_object (builder, "button_quit"));
	btn_clear = GTK_WIDGET(gtk_builder_get_object (builder, "button_clear"));
	btn_add = GTK_WIDGET(gtk_builder_get_object (builder, "button_add"));
	text_view = GTK_TEXT_VIEW(gtk_builder_get_object (builder, "text_view"));
	text_buffer = GTK_TEXT_BUFFER(gtk_builder_get_object(builder, "text_buffer"));

	gtk_builder_add_callback_symbol (builder, "button_clear_clicked_cb", G_CALLBACK(button_clear_clicked_cb));
	gtk_builder_add_callback_symbol (builder, "button_add_clicked_cb", G_CALLBACK(button_add_clicked_cb));
	gtk_builder_add_callback_symbol (builder, "button_quit_clicked_cb", G_CALLBACK(button_quit_clicked_cb));

	g_signal_connect(btn_clear, "clicked", G_CALLBACK(button_clear_clicked_cb), NULL);
	g_signal_connect(btn_quit, "clicked", G_CALLBACK(button_quit_clicked_cb), NULL);
	g_signal_connect(btn_add, "clicked", G_CALLBACK(button_add_clicked_cb), NULL);

    g_object_unref(builder);

	gtk_text_buffer_create_tag(text_buffer, "gap", "pixels_above_lines", 30, NULL);
	gtk_text_buffer_create_tag(text_buffer, "lmarg", "left_margin", 5, NULL);
	gtk_text_buffer_create_tag(text_buffer, "blue_fg", "foreground", "blue", NULL);
	gtk_text_buffer_create_tag(text_buffer, "gray_bg","background", "gray", NULL);
	gtk_text_buffer_create_tag(text_buffer, "italic", "style", PANGO_STYLE_ITALIC, NULL);
	gtk_text_buffer_create_tag(text_buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
	gtk_text_buffer_get_iter_at_offset(text_buffer, &iter, 0);

	gtk_text_buffer_insert(text_buffer, &iter, "Contingut inicial\n", -1);
	gtk_text_buffer_insert_with_tags_by_name(text_buffer, &iter, "Colored Text\n", -1, "blue_fg", "lmarg",  NULL);
	gtk_text_buffer_insert_with_tags_by_name(text_buffer, &iter, "Text with colored background\n", -1, "lmarg", "gray_bg", NULL);
	gtk_text_buffer_insert_with_tags_by_name(text_buffer, &iter, "Text in italics\n", -1, "italic", "lmarg",  NULL);
	gtk_text_buffer_insert_with_tags_by_name(text_buffer, &iter, "Bold text\n", -1, "bold", "lmarg",  NULL);

	gtk_widget_show_all(win);
	idTimeoutPipeNonBlockigRead = g_timeout_add (100, non_blocking_pipe_read, NULL);
	gtk_main();
}

void *secondary_thread(void *arg) {
    int server_socket, client_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
	char message[BUFFER_SIZE];
	char date_now[BUFFER_DATE_SIZE];
	time_t rawtime;
    struct tm * timeinfo;
	int serving;
	char *trimmed;

    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_FAILURE) {
        perror("socket");
        exit(FAILURE);
    }

	address.sin_family = AF_INET;

    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons( PORT );

	if ( bind(server_socket, (struct sockaddr *) &address, sizeof(address)) == BIND_FAILURE ) {
        perror("bind");
        exit(FAILURE);
    }

    if (listen(server_socket, 5) == LISTEN_FAILURE) {
        perror("listen");
        exit(FAILURE);
    } else {
		printf("\n\nServer listening on port 8080 ...\n");
	}

	if ((client_socket = accept(server_socket, (struct sockaddr *) &address, (socklen_t*) &addrlen ) ) == ACCEPT_FAILURE ) {
        perror("accept");
        exit(FAILURE);
    }

    time (&rawtime);
    timeinfo = localtime (&rawtime);

    strftime (date_now, BUFFER_DATE_SIZE, "%I:%M%p.",timeinfo);
	sprintf(message, "Hello from server. %s\n\n", date_now);
	strcpy(buffer, message);

	if ( send(client_socket, buffer, strlen(buffer), 0) == SEND_FAILURE) {
		perror("Send failed");
		exit(FAILURE);
	}

	serving = TRUE;

	while(serving) {
		memset(buffer, 0, BUFFER_SIZE);
		if( recv(client_socket, buffer, BUFFER_SIZE , 0) == RECV_FAILURE) {
			perror("recv failed");
			exit(FAILURE);
		}

		trimmed = trim(buffer);

		if (strcmp(trimmed, "END") == 0) {
		    serving = FALSE;
		}

		if ( write(fd[1], trimmed, strlen(trimmed)) == WRITE_ERROR ) {
			perror("pipe write error");
		}
	}

	close(fd[1]);

	close(client_socket);

	close(server_socket);

    pthread_exit(NULL);
}

void on_window_main_destroy() {
    gtk_main_quit();
}

int main(int argc, char *argv[]) {
    int err;
	int rc;
    void *status;
    pthread_t t_id;

	err = pipe(fd);

	if (err == PIPE_FAILURE){
        perror("creating pipe ");
        exit(FAILURE);
    }

	fcntl( fd[0], F_SETFL, fcntl(fd[0], F_GETFL) | O_NONBLOCK);

	err = pthread_create(&t_id, NULL, &secondary_thread, NULL);
	if (err != 0) {
        printf("\ncan't create thread :[%s]", strerror(err));
		exit(FAILURE);
	} else {
        printf("\n Thread created successfully\n");
	}

	init_widgets(argc, argv);

	rc = pthread_join(t_id, &status);

	if (rc) {
		printf("ERROR; return code from pthread_join() is %d\n", rc);
        exit(FAILURE);
	}

    printf("\n Main: completed join with thread %ld having a status of %ld\n\n", t_id,(long)status);

	return SUCCESS;
}

El plantejament del programa és semblant al del post anterior : un fil en el que s’executa el servidor, que és la funció void *secondary_thread(void *arg) entre les línies 150 i 227. És exactament la mateixa funció que al post anterior i valen els mateixos comentaris. I un fil lector, que llegeix els missatges que es troben a la cua.

Els canvis venen en el fil lector. En aquesta ocasió, igual com passava amb el post del ServerSocket amb interfície Swing, les actualitzacions de la GUI han d’executar-se des del dispatcher del GTK, i això vol dir que cal fer que les lectures de la cua no deixin bloquejada la GUI.

La solució ha estat la següent : per una banda faig que les lectures de la cua no siguin bloquejants. Si no hi ha res per llegir de la cua, la lectura no es queda esperant sino que segueix amb la següent instrucció. La lectura no bloquejant es defineix a la línia 246 :

fcntl( fd[0], F_SETFL, fcntl(fd[0], F_GETFL) | O_NONBLOCK);

La segona part és que cal repetir la lectura fins que hi hagi alguna cosa per llegir. Però no cal fer-ho constantment. El que es fa és un polling de la cua deu cops per segon. És més que suficient per al propòsit d’aquest experiment.

Aquest polling ha de fer-se des de dins del dispatcher de GTK, Això ho aconsegueixo amb un timer.

A la funció init_widgets (línies 107 a 148) és on inicialitzo la GUI i és on defineixo el timer que executa la tasca (la línia 146).

idTimeoutPipeNonBlockigRead = g_timeout_add (100, non_blocking_pipe_read, NULL);

El que fa g_timeout_add és cada 100 milisegons invoca la funció non_blocking_pipe_read. El tercer argument NULL són els paràmetres que es passen a la funció, en aquest cas, cap.

La funció non_blocking_pipe_read es defineix entre les línies 82 i 105. La funció ha de ser, com s’indica a Devhelp, una GSourceFunc :

GSourceFunc ()
gboolean (*GSourceFunc) (gpointer user_data);
Specifies the type of function passed to g_timeout_add(), g_timeout_add_full(), g_idle_add(), and g_idle_add_full().

Parameters
user_data
data passed to the function, set when the source was created with one of the above functions

Returns
FALSE if the source should be removed. G_SOURCE_CONTINUE and G_SOURCE_REMOVE are more memorable names for the return value.

La funció fa una lectura no bloquejant io si té alguna cosa a escriure, actualitza la GUI. Pot fer-ho perquè s’està executant dins el dispatcher del GTK.

Com diu la documentació, la funció ha de retornar TRUE (O G_SOURCE_CONTINUE) si el timer ha de seguir funcionant, o FALSE (o G_SOURCE_REMOVE) en cas contrari, és dir, quan es rep l’END per la cua.

Aquest muntatge funciona amb les proves que he fet en que el client envia missatges curts i amb la cadència d’un mecanografiat. Amb aquest requeriment el timer de una dècima és més que suficient per a proporcionar la sensació d’immediatesa.

Les cues són, probablement, el mecanisme més senzill de comunicació entre fils o processos. La cua, per la seva banda, té un buffer implícit de capacitat limitada i variable entre sistemes i versions. Com s’indica al manual de Linux, es podria canviar si calgués.

albert@artemis:~$ man 7 pipe

PIPE(7) Linux Programmer's Manual PIPE(7)
NAME
pipe - overview of pipes and FIFOs
...
Pipe capacity
A pipe has a limited capacity. If the pipe is full, then a write(2)
will block or fail, depending on whether the O_NONBLOCK flag is set
(see below). Different implementations have different limits for the
pipe capacity. Applications should not rely on a particular capacity:
an application should be designed so that a reading process consumes
data as soon as it is available, so that a writing process does not
remain blocked.
In Linux versions before 2.6.11, the capacity of a pipe was the same as the system page size (e.g., 4096 bytes on i386). Since Linux 2.6.11, the pipe capacity is 16 pages (i.e., 65,536 bytes in a system with a page size of 4096 bytes). Since Linux 2.6.35, the default pipe capac‐ ity is 16 pages, but the capacity can be queried and set using the fcntl(2) F_GETPIPE_SZ and F_SETPIPE_SZ operations. See fcntl(2) for more information.

I un apunt final. Al text view és possible passar-li formats al text. A la funció init_widgets, entre les línies 131 i 142 hi ha una mostra de com fer-ho.

C, sockets i fils

Els dos darrers posts han tracta sobre la implementació d’un senzill servidor basat en la classe ServerSocket de Java. Aquest servidor escolta les peticions entrants i quan estableix una connexió, saluda al client i es dedica a mostrar per pantalla, o a una gui, els missatges que aquest li envia, fins que el client talla la connexió.

El que proposo en aquest post és reimplementar el senzill servidor amb interfície en mode text fent servir el llenguatge C. C i Sockets. Ben bé un retorn als orígens. Allà on tot va començar, que diria Gerard Piqué.

Amb C sobre Linux, en el meu cas un Debian GNU/Linux 9.13 (stretch) i arquitectura x86_64, el que faig és un programa que llegeix d’una pipe (o FIFO, o tub, o cua) , i els mostra, els missatges enviats pels clients que des d’un fil dedicat un servidor escolta a través un socket; i els fica a la pipe.

Tres elements, doncs :

  • El fil per al servidor.
  • El fil principal que mostra els missatges.
  • La pipe que comunica tots dos fils.

Vet aquí el codi :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <ctype.h>

#define SUCCESS 0
#define FAILURE 1
#define PIPE_FAILURE -1
#define SOCKET_FAILURE -1
#define ACCEPT_FAILURE -1
#define LISTEN_FAILURE -1
#define BIND_FAILURE -1
#define SEND_FAILURE -1
#define RECV_FAILURE -1
#define WRITE_ERROR -1
#define READ_ERROR -1
#define TRUE 1
#define FALSE 0
#define BUFFER_SIZE 1024
#define BUFFER_DATE_SIZE 80
#define PORT 8080

char *ltrim(char *s);
char *rtrim(char *s);
char *trim(char *s);
void main_thread(void);
void *secondary_thread(void *arg);

pthread_t t_id;
char buffer[BUFFER_SIZE];
char buffer_read[BUFFER_SIZE];

int fd[2];

char *ltrim(char *s)
{
    while(isspace(*s)) s++;
    return s;
}

char *rtrim(char *s)
{
    char* back = s + strlen(s);
    while(isspace(*--back));
    *(back+1) = '\0';
    return s;
}

char *trim(char *s)
{
    return rtrim(ltrim(s));
}

void main_thread(void)
{
    pthread_t id = pthread_self();
    printf("\n main thread processing : %lu\n", id);
    int reading;
    int num_bytes;
    char *trimmed;

    reading = TRUE;

    while(reading) {
        memset(buffer_read, 0, BUFFER_SIZE);
        num_bytes = read(fd[0], buffer_read, BUFFER_SIZE);
        if (num_bytes == READ_ERROR) {
            perror("read failure");
        }

        trimmed = trim(buffer_read);

        printf("data : %s\n", trimmed);

        if (strcmp(trimmed, "END") == 0) reading = FALSE;
    }

    close(fd[0]);

    printf("\n main thread processing : %lu : done!\n", id);
}

void *secondary_thread(void *arg)
{
    int server_socket, client_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char message[BUFFER_SIZE];
    char date_now[BUFFER_DATE_SIZE];
    time_t rawtime;
    struct tm * timeinfo;
    int serving;
    char *trimmed;

    pthread_t id = pthread_self();

    if(pthread_equal(id,t_id)) {
        printf("\n thread processing : %lu\n", id);
    }

    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_FAILURE) {
        perror("socket");
        exit(FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons( PORT );

    if ( bind(server_socket, (struct sockaddr *) &address, sizeof(address)) == BIND_FAILURE ) {
        perror("bind");
        exit(FAILURE);
    }

    if (listen(server_socket, 5) == LISTEN_FAILURE) {
        perror("listen");
        exit(FAILURE);
    } else {
        printf("\n\nServer listening on port 8080 ...\n");
    }

    if ((client_socket = accept(server_socket, (struct sockaddr *) &address, (socklen_t*) &addrlen ) ) == ACCEPT_FAILURE ) {
        perror("accept");
        exit(FAILURE);
    }

    time (&rawtime);
    timeinfo = localtime (&rawtime);

    strftime (date_now, BUFFER_DATE_SIZE, "%I:%M%p.",timeinfo);
    sprintf(message, "Hello from server. %s\n\n", date_now);
    strcpy(buffer, message);

    if ( send(client_socket, buffer, strlen(buffer), 0) == SEND_FAILURE) {
        perror("Send failed");
        exit(FAILURE);
    }

    serving = TRUE;

    while(serving) {
        memset(buffer, 0, BUFFER_SIZE);
        if( recv(client_socket, buffer, BUFFER_SIZE , 0) == RECV_FAILURE) {
            perror("recv failed");
            exit(FAILURE);
        }

        trimmed = trim(buffer);

        if (strcmp(trimmed, "END") == 0) {
            serving = FALSE;
        }

        if ( write(fd[1], trimmed, strlen(trimmed)) == WRITE_ERROR ) {
            perror("pipe write error");
        }
    }

    close(fd[1]);

    close(client_socket);

    close(server_socket);

    printf("\n thread processing : %lu ; done!\n", id);

    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    int err;
    int rc;
    void *status;

    err = pipe(fd);
    if (err == PIPE_FAILURE) {
        perror("creating pipe ");
        exit(FAILURE);
    }

    err = pthread_create(&t_id, NULL, &secondary_thread, NULL);
    if (err != 0) {
        printf("\ncan't create thread :[%s]", strerror(err));
        exit(FAILURE);
    } else {
        printf("\n Thread created successfully\n");
    }

    main_thread();

    rc = pthread_join(t_id, &status);

    if (rc) {
        printf("ERROR; return code from pthread_join() is %d\n", rc);
        exit(FAILURE);
    }

    printf("\n Main: completed join with thread %ld having a status of %ld\n\n", t_id,(long)status);

    return SUCCESS;
}

Explicació del codi

Entre les línies 1 i 20 tinc els includes i els defines que són tractats pel preprocessador.

Els includes, en particular, contenen els protototips de les funcions i crides al sistema. Es fa la inclusió de pthread.h (POSIX threads), sys/socket.h (crides dels sockets), netinet/in.h (definició de la constant INADDR_ANY). Els habituals stdio.h (entrada i sortida estàndard) i stdlib.h (llibreria estàndard de C), unistd.h (crides a sistema estàndard de POSIX) , string.h (funcions de cadenes), time.h (funcions de data i hora) i ctype.h (per la funció isspace.h)

Al bloc define hi poso constants com la mida dels buffers utilitzats, el port del servidor, o el codi d’error de retorn de les crides de sockets, o de fitxers.

Entre 28 i 32 hi poso els prototips de les funcions propies

Entre 34 i 38 defineixo variables globals : els descriptor d’entrada i de sortida de la pipe, els buffers d’escriptura i lectura utilitzats i una variable per a mantenir l’id del fil.

Entre 40 i 57 una pràctica implementació de rtrim, ltrim i trim que he trobat Stackoverflow i que em faran servei per “netejar” d’ espais en blanc, salts de línia…. els missatges rebuts.

main thread

Entre 59 i 86 hi ha la funció main_thread. La funció s’enceta obtenint el pid actual per a demostrar el funcionament multifil, prepara el buffer de lectura des del socket i procedeix a llegir per l’extrem de lectura del socket. Pipe retorna l’array fd de dos descriptors, el primer fd[0] correspon a l’extrem de lectura de la pipe, i el segon fd[1] al d’escriptura. La lectura amb read és bloquejant en el cas que ens ocupa. Ara no té més importància, però en el proper post, en que faré servir una GUI amb GTK sí que en tindrà, i allà faré servir una lectura no bloquejant.

El missatge llegit es neteja d’espais en blanc o salts de línia i es mostra per pantalla. Si el missatge rebut és ‘END’ la bandera reading es posa a false i se surt del llaç de lectura.

Finalment es tanca la descriptor de lectura i es traça la sortida de la funció.

secondary thread

De 88 a 173 ve la funció secondary_thread. Aquesta funció s’executa en un thread propi. Per a poder ser llençada des de la funció pthread_create el prototip de la funció ha de seguir el model

void *nom_funcio(void *arg);

Les variables utilitzades per la funció es declaren entre les línies 90 i 98. Destacar el buffer de lectura i el buffer per formatar una cadena amb la data actual que s’envia als clients junt amb el missatge de benvinguda. A més també hi ha variables per a la data propiament dita, una bandera per aturar el llaç de lectura de missatges del socket, els socket i l’adreça del socket servidor,

Com a la funció main thread, es comença obtenint el pid del thread localment amb pthread_self, però ara es comparar amb el pid del thread obtingut durant la creació del fil amb pthread_create. Han de coincidir. Aquesta comparació es fa amb pthread_equal, que retorna un valor diferent de 0 (TRUE) en cas que els dos pid siguin iguals i zero (FALSE) en cas que siguin diferents. D’aquesta forma es demostra el comportament multifil. Per a reforçar la demostració, es mostra per pantalla el fil del thread, per a poder comparar-lo visualment amb el del fil principal.

Server socket

Immediatament comença el procés de creació de socket de la família Internet (AF_INET) i del tipus TCP (SOCK_STREAM), sobre el protocol de xarxa IP ( és el tercer argument, 0, de la crida socket de la línia 106).

L’adreça del socket es prepara ventre les línies 111 i 113. Destacar que a la línia 112 es declara que s’escoltarà per totes les interfícies locals, és dir, per totes les adreces IPv4 locals de la màquina. indicant quen l’adreça és INADDR_ANU, o sigui, 0.0.0.0 (veure https://en.wikipedia.org/wiki/0.0.0.0) .

Un cop creat el socket i l’adreça, es vinculen amb bind, línies 115 a 118, i es procedeis a esperar connexions entrants amb listen (línies 120 a 125). S’estableix una cua d’un màxima de 5 connexions en espera.

Quan finalment hi ha una connexió s’obté el socket de client amb accept, línies 127 a 130.

En aquest moment s’enceta la preparació del missatge de benvinguda, que conté l’hora actual i això es fa entre les línies 132 i 137.

I s’envia el missatge de benvinguda al client amb send (línies 139 a 142).

Entre 144 i 162 hi ha un llaç controlat per la variable serving que va escoltant i rebent les dades dela connexió amb el client (amb recv). Les dades llegides són netejades d’espais en blanc finalsx i salts de línia.

Si el missatge rebut es END la bandera de control serving es posa a FALSE

I s’escriu el missatge rebut per la banda d’escriptura de la cua (fd[1]).

Si s’ha rebut END, se surt del while i es procedeix a tancar el descriptor d’escriptura de la cua i els sockets servidor i de client.

Es traça la finalització del fil secundari i es surt del fil amb pthread_exit.

main

Tot plegat es llença des del main (línies 175 a 207).

Entre les línies 181 i 185 es crea el tub que permetrà comunicar els fils.

Entre 187 i 193 és crea i es llença l’execució del fil secundari, és dir l’execució de la funció servidora void*secondary_thread(void*arg) en el seu propi fil.

Des del fil principal es llená l’execució de la funció main_thread().

I després s’invoca pthread_join (línies 197 a 202) per garantir l’acabament ordenat .

Finalment es traça el join amb el fil secundari i la finalització de l’aplicació.

Proves

Pert a provar el programa cal compilar. He fet servir codelite i només m’ha calgut indicar a les propietats del linkador que calia utilitzar pthread. Com es veu a la imatge. Codelite ha generat el makefile necessari per a compilar

Pe a provar, com ens els posts anterior, només cal obrir un parell de terminals, en un executo l’aplicació i en l’altre nc localhost 8080.

Vet aquí el terminal amb nc localhost 8080 :

albert@artemis:~$ nc localhost 8080
Hello from server. 04:59PM.
hola
com va això ?
surto
END
albert@artemis:~$

i el terminal amb l’aplicació :

albert@artemis:~/workspace/codelite/lab-thread/lab_serversocket/Debug$ ls
lab_serversocket main.c.o main.c.o.d
albert@artemis:~/workspace/codelite/lab-thread/lab_serversocket/Debug$ lab_serversocket
Thread created successfully
main thread processing : 139703503939328
thread processing : 139703495878400
Server listening on port 8080 …
data : hola
data : com va això ?
data : surto
thread processing : 139703495878400 ; done!
data : END
main thread processing : 139703503939328 : done!
Main: completed join with thread 139703495878400 having a status of 0
albert@artemis:~/workspace/codelite/lab-thread/lab_serversocket/Debug$

ServerSocket a Java Swing amb SwingWorker

Al post anterior vaig fer un petit servidor java que es dedicava a escoltar els missatges dels clients i els mostrava per terminal. En aquest post agafo aquell servidor i li dono una interfície gràfica basada en Swing.

A la interfície gràfica hi poso una textArea on es mostraran els missatges. A més un parell de botons : Clear, per esborrar la text area i Quit, que mostra un petit popup que demana confirmació de tancament i, si és el cas, tanca l’aplicació.

He desenvolupat aquesta petita aplicació fent servir Netbeans 8.2 amb JDK 1.8. He triat Netbeans perquè el seu editor de GUI amb Swing és molt còmode d’utilitzar, i tot l’IDE, en general.

Aquesta transformació en aplicació Swing provoca que calgui reorganitzar els fils.

Els events de les aplicacións Swing són tractats al que es coneix com EDT (Event Dispatcher Thread).

És necessari que les actualitzacions de la GUI es facin des d’aquest fil.

D’altra banda, un procés llarg i de fons, com el del servidor, ha d’executar-se en el seu propi fil.

A Swing la classe SwingWorker és la forma preferida d’executar aquesta mena de processos ficant-los al mètode doInBackground. L’actualització de la GUI es pot fer des del SwingWorker utilitzant els mètodes publish i process que són invocats des de doInBackground.

Vet aquí com queda el Server.java

package lab.swingworker;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.List;
import javax.swing.JTextArea;
import javax.swing.SwingWorker;

public class Server extends SwingWorker<Void, String> {
    JTextArea jTextArea1;
    
    public Server(JTextArea jTextArea1) {
        this.jTextArea1 = jTextArea1;
    }

    @Override
    protected Void doInBackground() throws Exception {
        System.out.println("hello from SwingWorker");

        try {
            ServerSocket server = new ServerSocket(8081);
            System.out.println("waiting for connection...");
            try (Socket connection = server.accept()) {
                Writer out = new OutputStreamWriter(connection.getOutputStream());
                Reader in = new InputStreamReader(connection.getInputStream());
                Date now = new Date();
                System.out.println("Incoming connection.");
                out.write("Message from server " + now.toString() +"\r\n");
                out.flush();
                char[] buffer = new char[1024];
                int i = 0;
                while (in.read(buffer) !=  -1) {                    
                    String data = new String(buffer);
                    i++;
                    this.publish("Missatge " + i + " : " + data);
                    buffer = new char[1024];
                }
                System.out.println("End serving.");
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        } catch(IOException e) {
            System.out.println(e.getMessage());
        }

        jTextArea1.append("END");
        System.out.println("bye from SwingWorker");
        
        return null;
    }

    @Override
    protected void process(List<String> chunks) {
        for(String chunk: chunks) {
            jTextArea1.append( chunk );
        }
    }
}

De fet, el codi és més senzill que a la versió de terminal. Aquí simplement li dic que vull publicar un missatge rebut amb publish. Aleshores el mètode process actualitza la GUI amb la llista de tots els missatges rebuts fins el moment. La classe SwingWorker ja s’encarrega que process faci aquesta actualització dins l’EDT.

La classe LabSwingworker és la que s’encarrega de muntar la GUI. És on es troben els handlers dels events de la GUI.

Destacar com es pot seleccionar el loook & feel de l’aplicació.

També és des d’on es llença l’execució del SwingWorker : fixeu-vos en la invocació del mètode execute de la classe Server dins el constructor de labSwingworker. Fixeu-vos també en com el constructor de LabSwingworker és invocar dins d’un invokeLater al main. Aquest invokeLater és la forma garanteixo que la GUI s’executa dins l’EDT. En realitat, doncs, tinc tres fils : el del main, el de l’EDT generat amb l’InvokeLater, i el del SwingWorker propiament dit, llençat amb el mètode execute de la classe Server. Tanmateix, ha estat implícit, no ha calgut fer cap creació explícita de fils.

Vet aquí la classe LabSwingworker

package lab.swingworker;

import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;

public class LabSwingworker extends javax.swing.JFrame {

    public LabSwingworker() {
        initComponents();
        Server server = new Server(jTextArea1);
        server.execute();
    }

    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        jScrollPane1 = new javax.swing.JScrollPane();
        jTextArea1 = new javax.swing.JTextArea();
        jButton1 = new javax.swing.JButton();
        jButton2 = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("Lab - SwingWorker - ServerSocket");

        jTextArea1.setColumns(20);
        jTextArea1.setRows(5);
        jScrollPane1.setViewportView(jTextArea1);

        jButton1.setText("Quit");
        jButton1.setToolTipText("");
        jButton1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton1ActionPerformed(evt);
            }
        });

        jButton2.setText("Clear");
        jButton2.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton2ActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 521, Short.MAX_VALUE)
                    .addGroup(layout.createSequentialGroup()
                        .addGap(4, 4, 4)
                        .addComponent(jButton2)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                        .addComponent(jButton1)
                        .addGap(0, 0, Short.MAX_VALUE)))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 363, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jButton1)
                    .addComponent(jButton2))
                .addGap(18, 18, 18))
        );

        pack();
    }// </editor-fold>                        

    private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {                                         
        jTextArea1.setText("");
    }                                        

    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         
        int option = JOptionPane.showConfirmDialog(this, "Confirma que vol sortir?", "Confirmació de sortida", JOptionPane.OK_CANCEL_OPTION);
        
        if (option == JOptionPane.OK_OPTION) {
            System.exit(0);
        }
    }                                        

    public static void enableLookAndFeel() {
        try {
            for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                if ("GTK+".equals(info.getName())) {
                    UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (Exception e) {
            // If Nimbus is not available, you can set the GUI to another look and feel.
        }        
    }
    
    public static void main(String args[]) {
        enableLookAndFeel();
        
        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new LabSwingworker().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify                     
    private javax.swing.JButton jButton1;
    private javax.swing.JButton jButton2;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTextArea jTextArea1;
    // End of variables declaration                   
}

Per a obtenir la imatge amb la que obria aquest article he obert dos terminals. des del primer he llençat l’execució de l’aplicació, des de laltre he utilitzat nc localhost 8081 per a enviar missatges a l’aplicació. Vet aquí el terminal amb l’aplicació :

albert@artemis:~/workspace/netbeans/lab-swingworker/dist$ java -jar lab-swingworker.jar
hello from SwingWorker
waiting for connection…
Incoming connection.
End serving.
bye from SwingWorker
albert@artemis:~/workspace/netbeans/lab-swingworker/dist$

I el terminal amb l’nc

albert@artemis:~/workspace/netbeans/lab-swingworker/dist$ nc localhost 8081
Message from server Sat Aug 01 23:43:10 CEST 2020
hola!
com va això ?
espero que bé!
Es tractaria de posar un exemple de com funciona el SwingWorker
em sembla que ja és suficient
^C
albert@artemis:~/workspace/netbeans/lab-swingworker/dist$

Finalment, amb Quit abandono l’aplicació

ServerSocket, cues i fils.

Un exercici que, al meu entendre, haurien de fer en algun moment tots els estudiants d’informàtica, telecomunicacions i segurament també industrials, és el d’una aplicació que faci servir sockets.

El que m’ocupa en aquest post i els propers és justament això, fer una aplicació que escolti per sockets (un servidor, per tant) de tipus TCP i que mostri els missatges rebuts. De moment, només aquesta funcionalitat. Però per a fer-ho una mica més interessant, el servidor pròpiament dit s’executarà en un fil i en una altre fil hi posaré la part que es dedica a mostrar els missatges rebuts. El fil servidor passarà els missatges llegits del socket al fil que s’encarrega de mostrar-los mitjançant una cua (o pipe, o FIFO, o tuberia).

Per facilitat, tot això ho implementaré amb Java.

Vet aquí el codi :

package lab.threads;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author albert
 */
public class LabThreads {
    BlockingQueue queue;
    LabThreads.Producer producer;
    LabThreads.Consumer consumer;
    
    class Producer extends Thread {
        BlockingQueue queue;

        public Producer(BlockingQueue queue) {
            this.queue = queue;
        }
        
        @Override
        public void run() {
            System.out.println("hello from producer " + this.getId() );
            
            try {
                ServerSocket server = new ServerSocket(8081);
                System.out.println("waiting for connection...");
                try (Socket connection = server.accept()) {
                    Writer out = new OutputStreamWriter(connection.getOutputStream());
                    Reader in = new InputStreamReader(connection.getInputStream());
                    Date now = new Date();
                    System.out.println("Incoming connection.");
                    out.write("Message from server " + now.toString() +"\r\n");
                    out.flush();
                    char[] buffer = new char[1024];
                    int i = 0;
                    while (in.read(buffer) !=  -1) {
                        String data = new String(buffer);
                        i++;
                        queue.add("Missatge " + i + " : " + data);
                        buffer = new char[1024];
                    }
                    System.out.println("End serving.");
                } catch (IOException e) {
                    System.out.println(e.getMessage());
                }
            } catch(IOException e) {
                System.out.println(e.getMessage());
            }
            
            queue.add("END");
            System.out.println("Bye from producer " + this.getId() );
        }
    }

    class Consumer extends Thread {
        BlockingQueue queue;

        public Consumer(BlockingQueue queue) {
            this.queue = queue;
        }
        
        @Override
        public void run() {
            System.out.println("hello from consumer " + this.getId() );
            int i = 0;
            String received = "";
            while (!"END".equals(received)) {
                received = (String) queue.poll();
                if (received != null) {
                    i++;
                    System.out.println("received ( " + i + " ): " + received.trim());
                }
            }
            System.out.println("Bye from consumer " + this.getId() );
        }
    }
  
    public LabThreads() {
        queue = new LinkedBlockingDeque<String>();
        producer = new  LabThreads.Producer(queue);
        consumer = new  LabThreads.Consumer(queue);
    }

    public void startThreads() {
        producer.start();
        consumer.start();
    }
    
    public void joinThreads() {
        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException ex) {
            System.out.println(ex.getMessage());
        }
    }

    public static void main(String[] args) {
        System.out.println("\nInit");
        LabThreads lab = new LabThreads();
        lab.startThreads();
        System.out.println("\nThreads running(main)");
        lab.joinThreads();
        System.out.println("\nDone (main)");
    }
}

El codi és ben senzill de llegir. Es tracta de la classe LabThreads que és l’agregació d’una queue del tipus BlockingThread, i un parell de classes internes que estenen Thread que implementen el fil servidor, producer, i el fil que mostra els missatges rebuts, consumer. Els noms producer i consumer són ben escaients aquí perquè el plantejament fet és patró productor-consumidor.

Tant el productor com el consumidor veuen la cua, que reben com a paràmetre als respectius constructors.

A les classes internes no faig més que sobreescriure el mètode run(), on hi poso la funcionalitat respectiva.

Pel que fa al productor, creo una instància (server) de ServerSocket que escolta pel port 8081 (línia 37). El fil es queda esperant a server.accept() fins que arriba una petició entrant. Quan hi ha una petició entrant server.accept() , línia 39, retorna un objecte Socket (connection) del que n’obtinc els fluxos de lectura i escrtiptura i amb els que construeixo un lector i escriptor de fluxos (línies 40 i 41).

Un cop tinc el lector i l’escriptor els puc fer servir, primer per enviar un missatge de benvinguda al client (línies 44 i 45) i després per llegir els missatges (línies 46 a 48) que envia el client. Els missatges rebuts es fiquen com Strings a la cua (línies 49 a 51)

Tot el procés es repeteix (while entre les línies 48 i 53) fins que el client talla la connexió, moment en que la lectura retorna un -1. Se surt del while i es fica el missatge END a la cua, d’aquesta forma, el fil que mostra les dades sap que també ha d’acabar.

El consumidor no fas res més que estar atent a la cua (queue. poll de la línia 80) i mostra els missatges que hi van apareixent. El consumidor repeteix aquesta lectura dins que rep un END, moment en que també acaba.

Tot plegat arrenca amb el main que no fa més que instanciar LabThreads (línia 112), i llençar l’execució dels fils (línia 113). Els constructor de LabThreads crea una instància de LinkedBlockingDeque<String> i l’assigna a queue, i invoca als constructors dels fils (línies 97 i 98) passant-lis la cua tot just creada.

Quna el client interromp la comunicació, el fil del serversocket acaba, però abans envia un END per a que el fil que mostra els missatges també acabi.

Per a acabar ordenmadament, doncs, cal fer un pas final de join dels dos fils amb el mètode joinThreads (línia 115) que no fa més que producer.join i consumer join.

Es pot provar que tot funciona com està previst obrint l’aplicació java en un terminal, i en un altre terminal executant l’ordre “nc localhost 8081”

Per exemple, si faig aquesta entrada

albert@artemis:~/workspace/netbeans/lab-threads/src/lab/threads$ nc localhost 8081
Message from server Sat Aug 01 17:36:27 CEST 2020
Hola!
Això és una prova
faig servir nc localhost 8081
per a enviar missatges al servidor java
ok, acabo
^C
albert@artemis:~/workspace/netbeans/lab-threads/src/lab/threads$

Obtinc la següent sortida :

albert@artemis:~/workspace/netbeans/lab-threads/dist$ java -jar lab-threads.jar
Init
hello from producer 8
Threads running(main)
hello from consumer 9
waiting for connection…
Incoming connection.
received ( 1 ): Missatge 1 : Hola!
received ( 2 ): Missatge 2 : Això és una prova
received ( 3 ): Missatge 3 : faig servir nc localhost 8081
received ( 4 ): Missatge 4 : per a enviar missatges al servidor java
received ( 5 ): Missatge 5 : ok, acabo
End serving.
received ( 6 ): END
Bye from consumer 9
Bye from producer 8
Done (main)
albert@artemis:~/workspace/netbeans/lab-threads/dist$

Represa del blog. Un simulador de contagis (a voltes amb el Covid19) fet amb C++

El Covid-19 ha representat una mala jugada per mi, ja que aquest abril passat l’empresa en la que treballava, a conseqüència d’una caiguda dràstica en la càrrega de feina, va decidir desprendre’s de treballadors i em va tocar. Ara mateix estic buscant feina, si us cal un informàtic amb experiència, ja sabeu, doncs.

Com no se estar-me aturat, a més de la recerca activa de feina, també reprenc l’activitat del blog de tecnologia després de gairebé dos anys de no publicar res. Aquest període de dos anys de silenci els he aprofitat per avançar en el Màster Universitari d’Enginyeria informàtica amb la UOC que vaig començar al febrer de 2017. A un ritme tranquil, però constant, ja només em resten tres assignatures  i el treball de fi de màster per acabar. Si les coses van mitjanament bé, obtindré el títol a finals de  2021 o començament de 2022.

Reprenc el blog. Durant la crisis del Covid s’han publicat algunes simulacions molt interessants de com es produïa la propagació del virus, i de com la mobilitat influeix en la propagació. Això m’ha motivat a fer el meu propi simulador. He decidit fer-lo en C++ per a recuperar els reflexos en aquest llenguatge.

La idea ha estat crear un camp de NxN en el que he situat un total de NUM_PEOPLE individus, dels quals INITIAL_INFECTED estan infectats (INFECTED) i INITIAL_HEALTHY = NUM_PEOPLE – INITIAL_INFECTED estan sans (ACTIVE). Aleshores, faig una simulació de NUM_STEPS_SIMULATION on a cada pas de la simulació el mue faig es desplaçar aleatòriament cada individu. cada cop que dos individus entren en contacte, si un d’ells està infectat, encomana a l’altre. Al cap de MAX_STEPS_INFECTED passos de simulació, un individu o bé es cura (HEALED), o bé es mor (DECEASED). Els individus morts es retiren del tauler (REMOVED).

He creat una estructura per mantenir les dades d’un individu,

person.h

#ifndef PERSON
#define PERSON

#include
#include “person.h”
#include “position.h”

struct person {
int id;
int steps_infected;
Position pos;
std::string health_state;
};

#endif

L’estructura de dades de posició les he posat a la classe Position

position.h

#ifndef POSITION
#define POSITION

class Position {
public:
int row;
int column;
static Position add(Position pos, Position inc_pos);
static Position direction_to_inc_pos(int direction);
static int direction_to_inc_col(int direction);
static int direction_to_inc_row(int direction);
bool isContiguous(Position p);
};

#endif

Per a la implementació de la clsse he considerat que la direcció del moviment des de la posició actual ve donada per un número aleatori entre 0 i 7 (ambdós inclosos)

0   1   2
  \ | /
7 - x - 3
  / | \ 
6   5   4

La implementació de la classe és la següent:

position.cpp

#include
#include "position.h"

Position Position::add(Position pos, Position inc_pos) {
Position new_pos;

new_pos.column = pos.column + inc_pos.column;
new_pos.row = pos.row + inc_pos.row;

return new_pos;
}

Position Position::direction_to_inc_pos(int direction) {
Position new_inc;

new_inc.column = Position::direction_to_inc_col(direction);
new_inc.row = Position::direction_to_inc_row(direction);

return new_inc;
}

int Position::direction_to_inc_col(int direction) {
int inc_col = 0;

if ( (direction = 0) || (direction = 6) || (direction = 7) ) {
inc_col = -1;
}
if ( (direction = 2) || (direction = 3) || (direction = 4) ) {
inc_col = 1;
}

return inc_col;
}

int Position::direction_to_inc_row(int direction) {
int inc_row = 0;

if ( (direction = 0) || (direction = 1) || (direction = 2) ) {
inc_row = -1;
}
if ( (direction = 4) || (direction = 5) || (direction = 6) ) {
inc_row = 1;
}

return inc_row;
}

bool Position::isContiguous(Position p) {
bool ret = false;

int d_row = std::abs(Position::row – p.row);
int d_col = std::abs(Position::column – p.column);

if ( (d_row <= 1) && (d_col <= 1) ) ret = true;
if ( (d_row == 0) && (d_col == 0) ) ret = false;

return ret;
}

Finalment, les regles de la simulació i la seva execució es defineixen a la classe contagion_simulation

contagion_simulation.h

#ifndef CONTAGION_SIMULATION
#define CONTAGION_SIMULATION

#include
#include
#include “person.h”

class contagion_simulation {
public:
static const int N = 10;
static const int NUM_PEOPLE = 20;
static const int INITIAL_INFECTED = 5;
static const int INITIAL_HEALTHY = NUM_PEOPLE – INITIAL_INFECTED;
static const int P_DECEASE = 50;
static const int MAX_STEPS_INFECTED = 5;
static const int NUM_STEPS_SIMULATION = 100;

static const std::string ACTIVE;
static const std::string INFECTED;
static const std::string HEALED;
static const std::string DECEASED;
static const std::string REMOVED;

int matrix[N][N];
std::vector people;

void init_simulation(void);
void run_simulation(void);
void show_simulation(void);

private:
void init_matrix(void);
void init_person(int id, std::string healthy_state);
void one_step_simulation(void);
void people_moving(void);
void person_moving(int id);
void validate_pos_limits(Position& new_pos, Position pos);
int validate_row_limits(int new_row, int row);
int validate_col_limits(int new_col, int col);
void move_if_not_collission(struct person current_person, Position new_pos);
void infection(void);
void heal_or_decease(void);
void remove_deceased(void);
std::string calculate_final_state(void);
void count_totals(void);
};

#endif

I la implementació,

contagion_simulation.cpp

#include
#include
#include
#include "contagion_simulation.h"
#include "position.h"
#include "person.h"

const std::string contagion_simulation::ACTIVE = “Active”;
const std::string contagion_simulation::INFECTED = “Infected”;
const std::string contagion_simulation::HEALED = “Healed”;
const std::string contagion_simulation::DECEASED = “Deceased”;
const std::string contagion_simulation::REMOVED = “Removed”;

void contagion_simulation::init_simulation(void) {
std::cout << “init_simulation” << std::endl;
time_t t=time(0);
srand(t);
int num;

contagion_simulation::init_matrix();

int id_healthy;
for (num = 1; num <= contagion_simulation::INITIAL_HEALTHY; num++) {
id_healthy = num;
contagion_simulation::init_person(id_healthy, contagion_simulation::ACTIVE);
}

int id_infected;
for (num = 1; num <= contagion_simulation::INITIAL_INFECTED; num++) {
id_infected = contagion_simulation::INITIAL_HEALTHY + num;
contagion_simulation::init_person(id_infected, contagion_simulation::INFECTED);
}
}

void contagion_simulation::init_person(int id, std::string initial) {
int row = rand() % contagion_simulation::N;
int column = rand() % contagion_simulation::N;

if (contagion_simulation::matrix[row][column] == 0) {
Position new_position;
new_position.column = column;
new_position.row = row;
struct person new_person;
new_person.id = id;
new_person.steps_infected = 0;
new_person.health_state = initial;
new_person.pos = new_position;
contagion_simulation::people.push_back(new_person);
contagion_simulation::matrix[row][column] = id;
} else {
contagion_simulation::init_person(id, initial);
}
}

void contagion_simulation::init_matrix(void) {
int row;
int column;
for (row = 0; row < contagion_simulation::N; row++) {
for(column = 0; column < contagion_simulation::N; column++) {
contagion_simulation::matrix[row][column] = 0;
}
}
}

void contagion_simulation::show_simulation(void) {
std::cout << “show_simulation” <<
“——————————————-” << std::endl;
int i;
for (i = 0; i < contagion_simulation::NUM_PEOPLE; i++) {
struct person current_person = contagion_simulation::people.at(i);

std::cout << “Person id = ” << current_person.id << “\n” <<
” pos = (” << current_person.pos.row <<
“, ” << current_person.pos.column <<
“)\n” <<
” health = ” << current_person.health_state << “\n” <<
” days infected = ” << current_person.steps_infected << “\n” <<
“——————————————-” << std::endl;
}
}

void contagion_simulation::run_simulation(void) {
int step;

contagion_simulation::count_totals();

for(step =1; step <= contagion_simulation::NUM_STEPS_SIMULATION; step++) {
std::cout << “Step : ” << step << std::endl;
contagion_simulation::one_step_simulation();
}
}

void contagion_simulation::one_step_simulation(void) {
contagion_simulation::people_moving();
contagion_simulation::infection();
contagion_simulation::heal_or_decease();
contagion_simulation::remove_deceased();
contagion_simulation::count_totals();
}

void contagion_simulation::people_moving(void) {
int num;
for (num = 0; num < contagion_simulation::NUM_PEOPLE; num++) {
struct person current_person = contagion_simulation::people.at(num);
if (current_person.health_state != contagion_simulation::REMOVED) {
contagion_simulation::person_moving(num);
}
}
}

void contagion_simulation::person_moving(int num) {
struct person current_person = contagion_simulation::people.at(num);
Position pos = current_person.pos;
Position inc_pos;
Position new_pos;

int direction = rand() % 8;

inc_pos = Position::direction_to_inc_pos(direction);
new_pos = Position::add(pos, inc_pos);

contagion_simulation::validate_pos_limits(new_pos, pos);
contagion_simulation::move_if_not_collission(current_person, new_pos);
}

void contagion_simulation::validate_pos_limits(Position& new_pos, Position pos) {
new_pos.row = contagion_simulation::validate_row_limits(new_pos.row, pos.row);
new_pos.column = contagion_simulation::validate_col_limits(new_pos.column, pos.column);
}

int contagion_simulation::validate_row_limits(int new_row, int row) {
if ( (new_row < 0) || (new_row == contagion_simulation::N) )
new_row = row;

return new_row;
}

int contagion_simulation::validate_col_limits(int new_col, int col) {
if ( (new_col < 0) || (new_col == contagion_simulation::N) )
new_col = col;

return new_col;
}

void contagion_simulation::move_if_not_collission(struct person current_person, Position new_pos) {
if (contagion_simulation::matrix[new_pos.row][new_pos.column] == 0) {
contagion_simulation::matrix[current_person.pos.row][current_person.pos.row] = 0;
current_person.pos = new_pos;
contagion_simulation::matrix[new_pos.row][new_pos.column] = current_person.id;
}
}

void contagion_simulation::infection() {
int i;
for (i = 0; i < contagion_simulation::NUM_PEOPLE; i++) {
struct person person1 = contagion_simulation::people.at(i);
int j;
for (j = 0; j < contagion_simulation::NUM_PEOPLE; j++) {
struct person person2 = contagion_simulation::people.at(j);
if (person1.pos.isContiguous(person2.pos)) {
if ((person1.health_state == contagion_simulation::INFECTED) &&
(person2.health_state == contagion_simulation::ACTIVE)) {
contagion_simulation::people.at(j).health_state = contagion_simulation::INFECTED;
contagion_simulation::people.at(j).steps_infected = 0;
}
if ((person1.health_state == contagion_simulation::ACTIVE) &&
(person2.health_state == contagion_simulation::INFECTED)) {
contagion_simulation::people.at(i).health_state = contagion_simulation::INFECTED;
contagion_simulation::people.at(i).steps_infected = 0;
}
}
}
}
}

void contagion_simulation::heal_or_decease() {
int i;
for (i = 0; i < contagion_simulation::NUM_PEOPLE; i++) {
if (contagion_simulation::people.at(i).health_state == contagion_simulation::INFECTED) {
contagion_simulation::people.at(i).steps_infected++;
if (contagion_simulation::people.at(i).steps_infected == contagion_simulation::MAX_STEPS_INFECTED) {
contagion_simulation::people.at(i).health_state = contagion_simulation::calculate_final_state();
}
}
}
}

void contagion_simulation::remove_deceased() {
int i;
for (i = 0; i < contagion_simulation::NUM_PEOPLE; i++) {
struct person current_person = contagion_simulation::people.at(i);
if (current_person.health_state == contagion_simulation::DECEASED) {
contagion_simulation::matrix[current_person.pos.row][current_person.pos.column] = 0;
contagion_simulation::people.at(i).health_state = contagion_simulation::REMOVED;
contagion_simulation::people.at(i).pos.row = – contagion_simulation::N;
contagion_simulation::people.at(i).pos.column = – contagion_simulation::N;
}
}
}

void contagion_simulation::count_totals(void) {
int i;
int count_active = 0;
int count_healed = 0;
int count_infected = 0;
int count_deceased = 0;
for (i = 0; i < contagion_simulation::NUM_PEOPLE; i++) {
struct person current_person = contagion_simulation::people.at(i);
if (current_person.health_state == contagion_simulation::REMOVED) count_deceased++;
if (current_person.health_state == contagion_simulation::ACTIVE) count_active++;
if (current_person.health_state == contagion_simulation::HEALED) count_healed++;
if (current_person.health_state == contagion_simulation::INFECTED) count_infected++;
}

std::cout << “——————————————-\n” <<
” Active = ” << count_active << “\n” <<
” Healed = ” << count_healed << “\n” <<
” Infected = ” << count_infected << “\n” <<
” Deceased = ” << count_deceased << “\n” <<
“——————————————-” < contagion_simulation::P_DECEASE) {
state = contagion_simulation::DECEASED;
} else {
state = contagion_simulation::HEALED;
}

return state;
}

Finalment, la simulació es llença des del main.cpp

main.cpp

#include
#include "contagion_simulation.h"

#define SUCCESS 0
#define FAILURE -1

int main(int argc, char *argv[]) {
contagion_simulation simulation;

simulation.init_simulation();

simulation.run_simulation();

simulation.show_simulation();

return SUCCESS;
}

Possibles millores
El programa no és interactiu. Es podria fer que les dades es demanessin a través d’algun tipus d’interfície: visual, o des d’un fitxer, o per la línia d’ordres.
També, el resultat es mostra en mode text. Es podria fer que el resultat es visualitzés sobre un canvas, o es podria generar la seqüència d’imatges de cada pas, o un clip de vídeo amb la seqüència completa, o es podrien emmagatzemar les dades a una bd per fer el seu tractament posterior amb eines d’anàlisi de dades com R, o Python.
Es podria millorar el moviment dels individus que ara és d’un pas en una de les 8 direccions triada aleatoriament. Es podria fer, per exemple, que durant un determinat nombre de passos l’individu nomésés mogués en una direcció, o es podria fer que cada individu tingués unes rutes pròpies.
Per cert, ara que em miro el codi, faig autocrítica i m’adono que hauria d’haver seguit un estil de codificació ben definit. A Java el tinc clarissim. Amb C++, no tant.  Hauria pogut seguir un esquema semblant al de java o, millor encara, adoptar un estil estàndard com, per exemple, l’especificat a la Google C++ Style Guide.

Un servidor SSH a la tauleta Android

Tota tauleta Android té un Linux amagat al seu interior.

Per arribar fins aquest Linux hi han diverses opcions. La que he trobat més senzilla és mitjançant l’app Termux.

Termux

Termux és una app que es pot instal·lar des del Google Play.

Segons ens diu la seva pàgina:

“Termux combina una potent emulació de terminal amb una extensa col·lecció de packages de Linux :

• Gaudeix de les shells bash i zsh.
• Edita fitxers amb nano i vim (o emacs, però en aquest cas és imprescindible un teclat!)
• Accedeix a servidors remots mitjançant ssh (o, afegeixo, aixeca un servidor ssh i accedeix remotament a la teva tauleta/telèfon!)
• Desenvolupa en C/C++ amb les eines clang, make i gdb.
• Fes servir la consola Python com una calculadora de butxaca (o escriu scripts de propòsit general amb Python).
• Fes check out o clona projectes de repositoris remots amb git o amb subversion.
• Juga amb aventures conversacionals basades en text (us sona la saga Zork?) amb l’interpret frotz.”

Tot això -punt important- sense necessitat de rootejar la tauleta.

Instal·lar Termux no té cap secret. A més de Termux,  convé instal·lar l’app Termux:API que complementa a Termux :

“Termux:API proporciona accés per la línia de comandes a les API’s del disposotiu :

* lectura i enviament de sms des del terminal.
* Accés al GPS des dels scripts.
* Aplicació del dispositiu text-to-speech als resultats de les comandes.
* Vibració del dispositiu.
* Accés al clipboard per als scripts.
* Accés a la llista de contactes per als scripts.”

Per un petit preu, també s’ofereixen altres apps que encara fan més útil Termux.

– Termux:Boot : permet executar scripts quan el dispositiu arrenca.
– Termux:Float : Permetr executar Termuxa una finestra flotant.
– Termux:Styling _ squemes de color i customització de l’aparença delterminal.
– Termux:Task : permet invocar executables de Termux des de Tasker i apps compatibles
– Termux:Widget : permet arrencar scripts de Termux des de la pantalla inicial de la tauleta

La Wiki de Termux és força completa i entenedora. Dins la Wiki de Termux també trobem una completa secció de FAQs.

El que m’ha interessat més ha estat la possibilitat de disposar d’un repositori git a la tauleta, i que aquest repositori fos accessible des d’un altre ordinador mitjançant protocol ssh. És dir, el problema principal que m’he proposat resoldre amb Termux ha estat : com posar en marxa el servidor ssh a la tauleta? idealment, amb el servidor ssh actiu a la tauleta, jo podria connectar-m’hi un client git des del portàtil, per exemple.

Instal·lació del programari

Primer de tot, cal instal·lar el programari necessari a la tauleta. La instal·lació de Termux des de Google Play no té cap dificultat. Un cop instal·lat, engego Termux…

Welcome to Termux!

Wiki: https://wiki.termux.com
Community forum: https://termux.com/community
IRC channel: #termux on freenode
Gitter chat: https://gitter.im/termux/termux
Mailing list: termux+subscribe@groups.io

Search packages: pkg search 
Install a package: pkg install 
Upgrade packages: pkg upgrade
Learn more: pkg help
bash-4.4$ 

Observo que només hi ha un sistema base i cal afegir tot el programari que emcalgui.

Executo les següents instruccions (un teclat extern USB o Bluetooth pot ser de molta ajuda!)

Afegir paquets és molt semblant al procediment que se segueix amb distribucions Linux com Debian.

Vull una shell bash, com al Linux del sobretaula:

bash-4.4$ pkg install bash
Hit:1 http://termux.net stable InRelease
Reading package lists... Done
Building dependency tree       
Reading state information... Done
36 packages can be upgraded. Run 'apt list --upgradable' to see them.
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages will be upgraded:
  bash
1 upgraded, 0 newly installed, 0 to remove and 35 not upgraded.
Need to get 360 kB of archives.
After this operation, 0 B of additional disk space will be used.
Get:1 http://termux.net stable/main arm bash arm 4.4.23-2 [360 kB]
Fetched 360 kB in 0s (1291 kB/s)
(Reading database ... 7367 files and directories currently installed.)
Preparing to unpack .../archives/bash_4.4.23-2_arm.deb ...
Unpacking bash (4.4.23-2) over (4.4.19) ...
Setting up bash (4.4.23-2) ...
bash-4.4$ 

Vull C/C++ i Python per a poder programar

– instal·lació de clang

bash-4.4$ pkg install clang
Hit:1 http://termux.net stable InRelease
Reading package lists... Done
Building dependency tree
Reading state information... Done
38 packages can be upgraded. Run 'apt list --upgradable' to see them.
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages will be upgraded:
clang
1 upgraded, 0 newly installed, 0 to remove and 37 not upgraded.
Need to get 11.1 MB of archives.
After this operation, 13.4 MB of additional disk space will be used.
Get:1 http://termux.net stable/main arm clang arm 6.0.1 [11.1 MB]
Fetched 11.1 MB in 6s (1664 kB/s)
(Reading database ... 7379 files and directories currently installed.)
Preparing to unpack .../archives/clang_6.0.1_arm.deb ...
Unpacking clang (6.0.1) over (5.0.1-1) ...
Setting up clang (6.0.1) ...
bash-4.4$

– instal·lació de python :

bash-4.4$ pkg install python
Hit:1 http://termux.net stable InRelease
Reading package lists... Done
Building dependency tree       
Reading state information... Done
37 packages can be upgraded. Run 'apt list --upgradable' to see them.
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages will be upgraded:
  python
1 upgraded, 0 newly installed, 0 to remove and 36 not upgraded.
Need to get 5564 kB of archives.
After this operation, 393 kB disk space will be freed.
Get:1 http://termux.net stable/main arm python arm 3.6.6 [5564 kB]
Fetched 5564 kB in 3s (1849 kB/s) 
(Reading database ... 7397 files and directories currently installed.)
Preparing to unpack .../archives/python_3.6.6_arm.deb ...
Unpacking python (3.6.6) over (3.6.4-1) ...
dpkg: warning: unable to delete old directory '/data/data/com.termux/files/usr/lib/python3.6/lib2to3/tests': Directory not empty
Setting up python (3.6.6) ...
Setting up pip...
Looking in links: /data/data/com.termux/files/usr/tmp/tmp599bopw8
Collecting setuptools
Collecting pip
Installing collected packages: setuptools, pip
  Found existing installation: setuptools 28.8.0
    Uninstalling setuptools-28.8.0:
      Successfully uninstalled setuptools-28.8.0
  Found existing installation: pip 9.0.1
    Uninstalling pip-9.0.1:
      Successfully uninstalled pip-9.0.1
Successfully installed pip-10.0.1 setuptools-39.0.1
bash-4.4$ 

Haureu notat que els missatges que estic mostrant no corresponen a una instal·lació inicial si no a un upgrade. És que, de fet, per a escriure aquest post estic tornant a passar l’instal·lador.

En resum : el procediment és el mateix que amb els gestor de paquets d’altres distribucions.

Vull el git per mantenir el meu codi ben versionat

– git

pkg install git

Vull un editor que em permeti fer de tot, i que a més sigui programable amb Lisp!

– emacs

pkg install emacs

Necessito relaxar-me i distreure’m amb jocs de ficció interactiva.

– frotz

pkg install frotz

Criptografia i ssh.

– openssl :

pkg install openssl

– openssh :

pkg install openssh

Altres paquets que poden ser interessants : el gpg, netcat, curl…

Si feu

pkg list-all

Obtindreu una llista dels paquets disponibles. En general, instal·lar un paquet és tan senzill com

pkg install nom_paquet

Configuració del servidor SSH

El client SSH ens queda instal·lat sense més que instal·lar el paquet openssh El que resta és configurar el servidor ssh per a fer accessible la tauleta des de l’exterior.

Em baso en el que hi ha a :

https://glow.li/technology/2015/11/06/run-an-ssh-server-on-your-android-with-termux/

i també a https://wiki.termux.com/wiki/Remote_Access

El primer que cal tenir en compte és que el Termux només es per a un usuari. Això vol dir que qualsevol accés des de l’exterior ha de fer-se amb l’usuari que termux ens ha proporcionat. Però com us adonareu, termux no ens dona el password d’aquest usuari. És dir: no podem fer un login amb usuari – password.

Necessàriament, doncs, hem de fer el login d’una altre forma. La solució és crear una parell clau privada – clau pública d’accés amb ssh-keygen.

Simplement, executo ssh-keygen amb les opcions per defecte.

En el meu cas, com que ja disposava d’un parell clau privada/clau pública i ja l’he distribuït, el que he fet és generar el parell de claus a una carpeta alternativa.

bash-4.4$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/data/data/com.termux/files/home/.ssh/id_rsa): /data/data/com.termux/files/home/alternative1_rsa
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /data/data/com.termux/files/home/alternative1_rsa.
Your public key has been saved in /data/data/com.termux/files/home/alternative1_rsa.pub.
The key fingerprint is:
SHA256:xMACExf/9or00y35to2/vw/ft3HBWKZypXOoWFJpAFk u0_a89@localhost
The key's randomart image is:
+---[RSA 2048]----+
|  +ooo..+E       |
|   o...+  . .    |
|     .. o  +   + |
|       o  o   X  |
|        S. o B + |
|       . .+ + o .|
|      .  o.+  ...|
|     . o..+ oo o=|
|      . o. ++o+=X|
+----[SHA256]-----+
bash-4.4$ 

Ara puc observar que han aparegut els fitxers id_rsa i id_rsa.pub a la carpeta ~/.ssh. Respectivament la clau privada i la clau pública que he generat.

bash-4.4$ ls -al ~/.ssh
total 32
drwx------    2 u0_a89   u0_a89        4096 Sep  1 22:33 .
drwx------    7 u0_a89   u0_a89        4096 Sep  2 17:20 ..
-rw-------    1 u0_a89   u0_a89           0 Sep  1 22:34 authorized_keys
-rw-------    1 u0_a89   u0_a89        3381 Sep  1 21:15 id_rsa
-rw-------    1 u0_a89   u0_a89         742 Sep  1 21:15 id_rsa.pub
-rw-r--r--    1 u0_a89   u0_a89         532 Sep  1 21:32 known_hosts
bash-4.4$ 

Ara vaig a provar que el servidor ssh funciona. Per a fer-ho, primer de tot l’engego (amb termux en mode bàsic, no tinc una altre opció que engegar manualment)

sshd

Vull provar connectant-me amb ssh des del mateix termux, per a fer-ho cal que la clau pública que acabo de generar estigui a la llista de claus autoritzades

Per això, simplement la concateno a authorized_keys que es troba a ~/.ssh

bash-4.4$ cat id_rsa.pub >> authorized_keys
bash-4.4$ chmod 600 authorized_keys
bash-4.4$ ls -al
total 36
drwx------    2 u0_a89   u0_a89        4096 Sep  2 18:42 .
drwx------    7 u0_a89   u0_a89        4096 Sep  2 17:20 ..
-rw-------    1 u0_a89   u0_a89         742 Sep  2 18:42 authorized_keys
-rw-------    1 u0_a89   u0_a89        3381 Sep  1 21:15 id_rsa
-rw-------    1 u0_a89   u0_a89         742 Sep  1 21:15 id_rsa.pub
-rw-r--r--    1 u0_a89   u0_a89         532 Sep  1 21:32 known_hosts
bash-4.4$ 

Arribats a aquest punt, ja puc provar el meu servidor ssh

bash-4.4$ ssh ip_tauleta -p 8022
Welcome to Termux!

Wiki:            https://wiki.termux.com
Community forum: https://termux.com/community
IRC channel:     #termux on freenode
Gitter chat:     https://gitter.im/termux/termux
Mailing list:    termux+subscribe@groups.io

Search packages:   pkg search 
Install a package: pkg install 
Upgrade packages:  pkg upgrade
Learn more:        pkg help
$ 

Accés des de l’exterior

A la vista de com ha funcionat aquest primer accés, ja puc veure quin és el procediment per accedir remotament al servidor ssh de la tauleta: cal que a les authorized_keys de la tauleta hi hagi la clau pública de l’usauri de l’ordinador remot amb el que penso accedir.

Això vol dir que si vull accedir a la tauleta des del meu sobretaula, el procediment és el següent:

1 – he de generar una parella clau privada-clau pública amb ssh-keygen al sobretaula
2 – aleshores, he de concatenar la clau pública generada a l’authorized_keys de la la tauleta.
3 – reinicio sshd.
Per engegar el servidor :

sshd

Per controlar-ne el funcionament :

logcat -s 'syslog:*'

Per aturar el servidor :

pkill sshd

He de copiar la clau pública del sobretaula a la tauleta. La solució més primitiva és fer servir un pendrive usb, passant físicament la clau des del sobretaula a la tauleta. Però, certament, aquest mètode no és gaire elegant.

Hi ha una opció millor : Des de la tauleta puc connectar-me amb scp o sftp al sobretaula (evidentment, sempre que el sobretaula també tingui un servidor ssh! en funcionament)

$ sftp albert@ip_sobretaula
Connected to albert@ip_sobretaula.
sftp> ls
Baixades                               Escriptori                             ...      
sftp> cd .ssh
sftp> ls
authorized_keys   authorized_keys~  id_rsa            id_rsa.pub        known_hosts       
sftp> get id_rsa.pub id_rsa_sobretaula.pub
Fetching /home/albert/.ssh/id_rsa.pub to id_rsa_sobretaula.pub
...
sftp> exit
$ ls -al
total 36
drwx------    2 u0_a89   u0_a89        4096 Sep  2 22:25 .
drwx------    7 u0_a89   u0_a89        4096 Sep  2 22:23 ..
-rw-------    1 u0_a89   u0_a89         742 Sep  1 22:06 authorized_keys
-rw-------    1 u0_a89   u0_a89        3381 Sep  1 21:15 id_rsa
-rw-------    1 u0_a89   u0_a89         742 Sep  1 21:15 id_rsa.pub
-rw-------    1 u0_a89   u0_a89         740 Sep  2 22:25 id_rsa_sobretaula.pub
-rw-r--r--    1 u0_a89   u0_a89         532 Sep  1 21:32 known_hosts
$ 

Cal indicar que des del sobretaula, també hi ha la possibilitat de fer servir ssh-copy-id per passar la clau pública tot just generada a un servidor remot. (consulteu https://www.ssh.com/ssh/copy-id).

ssh-copy-id

Es pot fer amb una instrucció com aquesta:

ssh-copy-id -i ~/.ssh/mykey user@host

Aquesta instrucció accedeix al host remot i afegeix la clau pública (*) de mykey al fitxer authorized_keys del host remot.

(*) La clau pública! La clau privada, com el seu nom indica, és privada i ningú més que el propietari l’ha de conèixer!

Si li cal, ssh-copy-id demanarà la forma d’autenticar-se amb el host remot. En el cas de Termux, això planteja el problema que no es coneix el password de l’usuari de Termux.

Arribats a aquest punt, ja tinc la clau pública del sobretaula a la tauleta i l’he concatenat a l’authorized_keys. Reinicio el servidor sshd i, des del sobretaula provo a accedir :

albert@artemis:~$ ssh 192.168.0.112 -p 8022
Welcome to Termux!

Wiki:            https://wiki.termux.com
Community forum: https://termux.com/community
IRC channel:     #termux on freenode
Gitter chat:     https://gitter.im/termux/termux
Mailing list:    termux+subscribe@groups.io

Search packages:   pkg search 
Install a package: pkg install 
Upgrade packages:  pkg upgrade
Learn more:        pkg help
$ 

Ja puc accedir a la tauleta des del sobretaula.

Seguint el mateix procediment puc donar accés ssh a la tauleta des de qualsevol altre equip. El truc és generar el parell clau privada/clau pública a cada ordinador des del que em vulgui connectar i concatenar la clau pública obtinguda a l’authorized_keys de la tauleta.

Accés Git al repositori de la tauleta

Finalment, una aplicació pràctica. Inicialitzo un repositori git a la tauleta. En el meu cas he creat una carpeta workspace amb diferents subcarpetes per a desar-hi projectes en diferents llenguatges. He inicialitzat un repositori git a aquesta carpeta workspace.

$ ls 
albert                alternative1_rsa      alternative1_rsa.pub  hosts                 prova.txt             storage               workspace
$ cd workspace/
$ ls -al
total 40
drwx------   10 u0_a89   u0_a89        4096 Sep  1 20:53 .
drwx------    7 u0_a89   u0_a89        4096 Sep  2 22:30 ..
drwx------    7 u0_a89   u0_a89        4096 Sep  1 20:58 .git
drwx------    2 u0_a89   u0_a89        4096 Aug 19 12:48 as
drwx------    2 u0_a89   u0_a89        4096 Aug 19 12:49 awk
drwx------    2 u0_a89   u0_a89        4096 Dec 16  2017 bash
drwx------    2 u0_a89   u0_a89        4096 Feb 20  2018 c
drwx------    2 u0_a89   u0_a89        4096 Feb 20  2018 cc
drwx------    2 u0_a89   u0_a89        4096 Dec 16  2017 elisp
drwx------    4 u0_a89   u0_a89        4096 Aug 19 12:49 python
$ 

Ara me’n vaig al sobretaula, i obro, per exemple, l’Eclipse que té el client EGit.

Simplement he de configurar ela URI del repositori al que vull accedir. He de parar atenció a:

1 – Indicar protocol ssh.
2 – Indicar la ip de la tauleta i, molt important, el port 8022.
3 – He indicat el path complet fins el repositori
3 – No cal que indiqui usuari i password.

La imatge ho aclareix :

git01

La següent pantalla em mostra la branca master del repositori git de la tauleta :

Selecció_002

A partir d’aquest punt ja podria fer el checkout dels projectes.

Molt important : per a que tot això funcioni cal que les IP dels diferents dispositius involucrats siguin fixes. Això vol dir que

1 – Al router de la vostra xarxa local cal donar IP fixes (dins del rang intranet, òbviament, és dir 192.168.x.x) als diferents ordinadors , impresores, smartphones o tauletes que pugueu tenir connectats

2 – Cal indicar a les tauletes que facin servir sempre la mateixa IP que li heu fet correspondre al router  per connectar-se a la vostra xarxa local.

Encoder-Decoder Codi Binari – Codi Gray

Reprenc el bloc després d’una aturada d’un any!

He dedicat bona part del temps que dedicava al bloc a estudiar. Des de fa un any i mig estic matriculat als estudis de Màster d’Enginyeria Informàtica de la UOC. He tornat a la Universitat gairebé trenta anys després de posar-hi el peu per primer cop!

Fa poc més d’un parell d’anys les circumstàncies professionals em van empènyer a canviar de feina. Una de les conseqüències d’aquell canvi va ser que em va semblar que era un bon moment per capitalitzar l’experiència professional acumulada després de… gairebé vint-i-cinc anys al sector TIC, dels quals més de setze específicament a la consultoria.

He de dir que em va molt bé, per ara, i que duri. També cal dir que m’ho estic prenent amb calma: només faig dues assignatures per semestre.

Tanmateix, la quantitat de temps que cal dedicar als treballs i pràctiques, només amb dues assignatures, és prou important. Tant que en tot un any no m’he vist amb cor de dedicar temps al bloc d’apunts de tecnologia.

idealment, a meva intenció és que bloc i estudis siguin activitats sinèrgiques. No és el cas d’avui, però alguns dels treball que ja he fet per a la UOC donen per a un post interessant, i potser els adaptaré i els acabaré publicant aquí. En aquest sentit, a diferència dels polítics del PP, el meu màster me l’estic currant…  i puc aportar proves.

Fetes aquestes explicacions, anem al gra. Avui presento un exercici senzill per reprendre el bloc : es tracta d’un decoder/encoder de codi Binari a codi Gray. El codi Gray és una d’aquelles coses que vaig aprendre a l’Enginyeria Tècnica de Telecos, quan estudiava electrònica digital allà pels finals dels vuitanta. Hi havia alguna cosa gairebé màgica en les simplificacions de circuits combinacionals fent servir mapes de Karnaugh. La màgia és que als mapes  de Karnaugh els minterms es distribueixen per la taula seguint el codi de gray, aleshores, elements adjacents difereixen només en un bit, i això vol dir que es pot simplificar la variable binària corresponent al bit que canvia en aquells termes adjacents.

Però té més usos. Notablement, es fa servir per minimitzar els errors en les modulacions digitals QAM i Gray Coded M-PSK.

De Viquipèdia (https://ca.wikipedia.org/wiki/Codi_Gray) : « El codi binari reflectit o codi Gray, nomenat així en honor de l’investigador Frank Gray, és un sistema de numeració binari en què dos valors successius difereixen només en un dels seus dígits.

El codi Gray va ser dissenyat originalment per prevenir senyals espuris dels switches electromecànics. Actualment és utilitzat per a facilitar la correcció d’errors en els sistemes de comunicacions, com ara alguns sistemes de televisió per cable i la televisió digital terrestre. »

Vet aquí un vídeo que explica perquè el codi gray és tan útil en els codificadors de posició rotatoris (elimina els valors espuris que podria introduir la codificació binària directa) :

De fet, la idea original d’aquest post era implementar un control rotatori basat en el codi Gray amb Arduino . Queda per a més endavant.

Anem per feina, el que vull fer és un codificador / decodificador de binari a gray, és dir, vull un mètode al que li passo un número enter de n bits, i vull que em torni un array amb els bits del codi gray corresponent al número rebut.

també vull el mètode que faci el camí de tornada, és dir, vull el mètode al que li passi un array de bits amb el codi gray d’un número de n bits, i vull que em torni un array amb els bits de la codificació binària directa d’aquell número.

L’algoritme per a calcular el codi gray / codi binari és, en realitat, molt senzill. Està ben explicat a la wiki. La gràcia de tot això és que abans de consultar-ho a la wiki he dedicat un temps a veure si me’n recordava com es feia aquesta codificació i descodificació. Després d’algunes proves (de les que en trobareu rastre al meu GitHub), finalment me n’he sortit. Vet aquí el resultat :

Vet aquí el codi Python

Coder

def calculate_gray(number, n):
    bits = []
    gray = []
    
    remainder = number
    for nn in range(0, n):
        [quotient, remainder] = divmod(remainder, 2 ** (n - 1 - nn))
        bits.append(quotient)
        if nn == 0:
            gray.append(bits[0])
        else:
            gray.append(bits[nn - 1] ^ bits[nn])      
            
            
    return [bits, gray]

i decoder

def calculate_binary(gray, n):  # gray is an array of bits
    bits = []

    for pos in range(0, n):
        if pos == 0:
            bits.append(gray[0])
        else:
            bits.append(bits[pos - 1] ^ gray[pos])      
            
    return bits

i una taula de prova

if __name__ == '__main__':
    print "print a Gray table for n bits"
    n = raw_input('Number of bits? ')
    n = int(n)
    print "number of bits : %d" % n   
    
    bits = []
    gray = []
    grayTable = []

    for i in range(0, 2 ** n):
        [bits, gray] = calculate_gray(i, n)
        grayTable.append(gray)
        strBits = ''.join(map(lambda (x) : chr(ord('0') + x), bits))
        strGray = ''.join(map(lambda (x) : chr(ord('0') + x), gray))
        
        print "%5d --> %s --> %s" % (i, strBits, strGray)
    
    print "-------------------------------------------"
    
    bits = []

    for i in range(0, 2 ** n):
        bits = calculate_binary(grayTable[i], n)
        strBits = ''.join(map(lambda (x) : chr(ord('0') + x), bits))
        strGray = ''.join(map(lambda (x) : chr(ord('0') + x), grayTable[i]))
        
        print "%5d --> %s --> %s" % (i, strGray, strBits)

    print "Done!"

Com a cosa molt bonica, indicaria l’us de lambdes i join per a passar l’array de bits a cadena de caràcters; i també la funció divmod, que utilitzada en cascada em permet obtenir els bits corresponents a l’enter que vull codificar.

El repositori Github és :

https://github.com/abaranguer/gray-code-py-version.git

Bé, tot plegat queda una mica curt, oi? el que he fet és escriure una segona versió del codi, però aquest cop amb llenguatge C. Així, de pas, l’he refrescat una mica.

Python és un llenguatge de molt alt nivell d’abstracció i m’ha permès implementar l’algoritme en un obrir i tancar d’ulls. La versió C, en canvi, ha estat més interessant. Vet aquí el codi :

#include 
#include 
#include 

#define BUF_SIZE 3

/* typedef */
typedef struct typeRetDivMod {
	int quotient;
	int remainder;
} RetDivMod;

typedef struct typeRetCalculateGray {
	int *bits;
	int *gray;
} RetCalculateGray;

/* prototypes */
int main(void);
RetCalculateGray calculate_gray(int number, int n);
int *calculate_binary(int *gray, int n);
RetDivMod divmod(int number, int divisor);
char *bitsToString(int *bits, int numBits);

/* functions */
int main() {
	char buf[BUF_SIZE];
	int n;
	int i;
	RetCalculateGray retValue;
	int *bitsFromGray;
	char *bits;
	char *bits2;
	char *gray;

	printf("Print a Gray table for n bits\n\n");
	printf("Number of bits? ");
	fgets(buf, BUF_SIZE, stdin);
	n = atoi(buf);
	printf("number of bits : %d\n", n);

	int range = pow(2, n);

	for (i = 0; i  %s --> %s --> %s\n", i, bits, gray, bits2);
		free(bits);
		free(gray);
		free(bits2);
		free(retValue.bits);
		free(retValue.gray);
		free(bitsFromGray);
	}

	printf("Done!\n");
	return 0;
}

RetCalculateGray calculate_gray(int number, int n) {
	int remainder;
	int nn;
	RetDivMod retDivMod;
	RetCalculateGray retValue;

	retValue.bits = (int *) malloc(n * sizeof(int));
	retValue.gray = (int *) malloc(n * sizeof(int));

	remainder = number;

	for (nn = 0; nn < n; nn++) {
		retDivMod = divmod(remainder, pow(2, n - 1 - nn));
		retValue.bits[nn] = retDivMod.quotient;
		remainder = retDivMod.remainder;
		if (nn == 0) {
			retValue.gray[nn] = retValue.bits[0];
		} else {
			retValue.gray[nn] = retValue.bits[nn - 1] ^ retValue.bits[nn];
		}
	}

	return (retValue);
}

int *calculate_binary(int *gray, int n) {
	int nn;
	int *bits;

	bits = (int *) malloc(n * sizeof(int));

	for (nn = 0; nn < n; nn++) {
		if (nn == 0) {
			bits[nn] = gray[0];
		} else {
			bits[nn] = bits[nn - 1] ^ gray[nn];
		}
	}

	return (bits);
}

RetDivMod divmod(int number, int divisor) {
	RetDivMod retDivMod;

	retDivMod.quotient = number / divisor;
	retDivMod.remainder = number % divisor;

	return retDivMod;
}

char *bitsToString(int *bits, int numBits) {
	int i;
	char *charBits;

	charBits = (char *) malloc(numBits * sizeof(char) + 1);
	for (i = 0; i < numBits; i++) {
		charBits[i] = '0' + bits[i];
	}
	charBits[numBits] = '\0';

	return charBits;
}

El repositori Github de la versió C :

https://github.com/abaranguer/gray-code-c-version.git

Per acabar, doncs, a la versió C hi han més coses a comentar. En destaco :

  • L’us de typedef i struct per a definir estructures que faig servir per moure arguments entre  main  i les funcions de coding i encoding.
  • Us de malloc i free per reservar / alliberar dinàmicament l’espai de memòria per als arrays dels bits.
  • La versió C de la funció divmod de Python.
  • L’ús de fgets per a prendre entrada per consola de forma segura aprofitant que fgets fa automàticament el control de desbordament del buffer d’entrada.

El joc Memory amb Python i Tkinter

En aquest post presento un joc de Memory, fet amb Pyhton i amb Tkinter.

El Memory és juga en un tauler de NxN cartes amb N parell. Hi han N²/2 cartes diferents, és dir, de cada carta n’hi han dues d’iguals. Les cartes estan barrejades i inicialment les N² cartes estan tapades. La mecànica del joc és anar destapant destapant parelles de cartes, si les cartes coincideixen, queden destapades, si no, es tornes a tapar i es continua amb una altre parella. Es repeteix fins que totes les parelles estan destapades.
L’objectiu del joc és haver de repetir el mínim nombre de cops. Per aconseguir-ho, doncs, cal memoritzar on són les cartes. Es tracta, doncs, d’un exercici de memorització.

El cas és que a Internet vaig trobar una imatge amb 50 súper-herois i súper-malvats dels còmics de Marvel i gairebé veient la imatge se’m va acudir fer el joc de cartes.

Vet aquí la imatge :

Abans de començar, però, vaig rumiar la mecànica del joc. El resultat va ser aquesta versió en mode text

Memory en mode text

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

import random
import os
import time

instructions = '''
Memory
------

Memory és el joc clàssic de recordar les parelles.
Tenim un tauler de 4x4, 6x6 o 8x8 caselles. 
Cada casella es correspon amb un número.
Cada casella amaga el nom d'un objecte. Hi han 8, 16 o 32 objectes diferents.
Cada objecte apareix dues vegades.
En cada torn, el jugador ha de destapar dues caselles. 
Si a ambdues caselles hi ha el mateix objecte, les caselles queden destapades.
Si no, després d'un temps de 3 segons, les caselles tornen a tapar-se
El cicle es repeteix fins que el jugador ha destapat totes les caselles 
'''

class Card:
    nom = ""
    tapada = True

def initGame(n):
    cards = initCards()
    table = initTable(cards, n)
    return {"table":table, "cards":cards}
    
def initCards():
    cards = [Card() for i in range(0, 32)]
    cards[0].nom = 'Wolverine'
    cards[1].nom = 'Psylocke'    
    cards[2].nom = 'Storm'
    cards[3].nom = 'Jubilee'    
    cards[4].nom = 'X-23'
    cards[5].nom = 'Jean Grey'    
    cards[6].nom = 'Dark Fenix'
    cards[7].nom = 'Cyclops'    
    cards[8].nom = 'Gambit'
    cards[9].nom = 'Angel'    
    cards[10].nom = 'Beast'
    cards[11].nom = 'Nightcrawler'    
    cards[12].nom = 'Mystique'
    cards[13].nom = 'Professor X'    
    cards[14].nom = 'Magneto'
    cards[15].nom = 'Rogue'    
    cards[16].nom = 'Deadpool'
    cards[17].nom = 'Captain America'    
    cards[18].nom = 'Thor'
    cards[19].nom = 'Iron Man'    
    cards[20].nom = 'Spiderman'
    cards[21].nom = 'Black Widow'    
    cards[22].nom = 'Scarlet Witch'
    cards[23].nom = 'Ant Man'    
    cards[24].nom = 'Hulk'
    cards[25].nom = 'Colossus'    
    cards[26].nom = 'Venom'
    cards[27].nom = 'Doctor Strange'    
    cards[28].nom = 'Green Goblin'
    cards[29].nom = 'Ultron'    
    cards[30].nom = 'Vision'
    cards[31].nom = 'Black Panther'    
    return cards

def clrscr():
    os.system("clear")

def initTable(cards, n):
    table = [None for i in range(0, n * n)]
    for i in range(0, n * n / 2):
        while True:
            pos1 = random.randint(0, n * n - 1)
            pos2 = random.randint(0, n * n - 1)
            if (table[pos1] == None) and \
               (table[pos2] == None) and \
               (pos1 != pos2):
                table[pos1] = cards[i]
                table[pos2] = cards[i]
                break;
    return table

def showTable(table, n):
    for i in range(0, n * n):
        if not table[i].tapada:
            print "casella %d : %s" % (i + 1), table[i].nom

def getCells(table, n):
    while True:
        try:
            p1 = int(raw_input('\nguess cell 1 : '))
            p2 = int(raw_input('guess cell 2 : '))
            if not (p1 in range(1, n * n + 1)) or \
               not (p2 in range(1, n * n + 1)):
                print "Only numbers 1 to %d " % n * n
                continue
            if (not table[p1-1].tapada) or (not table[p2-1].tapada):
                print "Only closed cells"
                continue
            break
        except:
            print "Only integer values 1 to %d" % n * n
    return [p1-1, p2-1]

def analyzeCells(table, cells, n):
    endGame = True
    pos1 = cells[0]
    pos2 = cells[1]
    nom1 = table[pos1].nom 
    nom2 = table[pos2].nom 
    
    print "cell %d : %s" % (pos1 + 1, nom1)
    print "cell %d : %s" % (pos2 + 1, nom2)

    if (nom1 == nom2):
        table[pos1].tapada = False 
        table[pos2].tapada = False

    time.sleep(3)
    clrscr()
    
    for i in range(0, n * n):
        endGame = endGame and (not table[i].tapada)
          
    return not endGame

def showTable(table, n):
    for i in range(0, n * n):
        if not table[i].tapada:
            print "Posició %d : %s" % ((i + 1), table[i].nom)
        
def gameLoop(gameObjects, n):
    table = gameObjects.get("table")
    showTable(table, n)
    cells = getCells(table, n)
    continueGame = analyzeCells(table, cells, n)
    return continueGame

if __name__ == "__main__":
    clrscr()
    continueGame = True
    print(instructions)
    while True:
        n = raw_input("dimension NxN (4,6,8) ? ")
        if n in ['4','6','8']:
            break

    n = int(n)
    
    gameObjects = initGame(n)
    while continueGame:
        continueGame = gameLoop(gameObjects, n)

    print "Well done!"

El joc no té massa truc. Dins del if __name__==”__main__”: mostro les instruccions, espero a rebre un vlaor vàlid per N, i inicialitzo el joc.

L’array cells de 32 posicions manté la llista de cartes d’herois/malvats. Inicialitzo aquest array amb initCards. Noteu l’ús de la python comprehension per a inicialitzar l’array. També hagués pogut crear-lo inicialitzant amb [] i omplint-lo després amb append.

L’array table manté el tauler NxN. Aquest array es carrega amb initTable. Faig servir un algoritme molt simple de força bruta per omplir-lo de parelles : simplement trio un parell d’ubicacions a l’atzar dins del rang del tauler i comprovo si no estan repetides i si estan disponibles.

Un cop disposo de cells i table, ja només cal encetar la iteració del joc, és dir : demanar un parell de posicions, verificar que son vàlides, comprovar quina parella amaga i decidir si s’ha trobat una parella i s’ha d’acabar el joc, o si no és una parella vàlida i cal mostrar durant tres segons, abans d’esborrar la pantalla i continuar preguntant. Aquesta lògica es codifica a analyzeCells dins gameLoop.

És molt simple. L’aplicació fa servir entrada i sortida simpl per pantalla (es podria haver fet amb la llibreria ncurses). L’esborrat de pantalla es va invocant el programa clear amb os.system(“clear”), i l’espera de tres segons es fa amb time.sleep(3).

Amb aquesta versió en mode text, ja tinc les bases per a la versió amb GUI.

Memory amb GUI Tkinter

A la versió Tkinter he afegit algunes millores.

– Un tauler addicional de 10×10 i tinc, per tant, quatre possibles mides : 4×4, 6×6, 8×8 i 10×10
– Gestió per menú que permet controlar el final del joc, o començar-ne un de nou.
– una indicació del nombre de clicks utilitzats

Tinc, doncs, quatre frames :

– la finestra de l’aplicació, amb el menú que és on s’obren els frames de l’aplicació :

– el frame de nou joc, on es pot triar la mida del tauler

– el tauler NxN amb el joc pròpiament dit.

– la pantalla de final de joc on es diu quants clicks han calgut per destapar totes les cartes

Cal tenir present, a l’hora de treballar amb Tkinter que només pot haver un mainloop, l’aplicació es desenvolupa dins d’aquest mainloop. El mainloop és el fil principal de l’aplicació. Això és important perquè prefigura com ha de ser l’aplicació.

Les imatges

En un experiment inicial he fet servir Image i TkImage de PIL (Python Image Library) per a carregar directament la imatge jpg i processar-la per a retallar (crop) els de cada carta. Al final he optat per una altre alternativa : generar la imatge ppm de cada heroi/malvat per a fer-la servir amb un PhotoImage de Tkinter. Aquest objecte PhotoImage és estàndar del Tkinter, a diferència de les imatges amb PIL, llibreria que cal importar explícitament i que aporta funcionalitat de processament d’imatges. Tkinter.PhotoImage no permet gran cosa més que carregar la imatge i mostrar-la, a més només treballa amb imatges ppm, pgm i gif.

Per a crear les imatges ppm he fet servir aquest programa _

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

import Tkinter as tk
from PIL import Image, ImageTk


# 450 293
xdim = 45
ydim = int(293.0 / 5.0)
cropDimensions= (xdim, ydim)

images = []

imageFace = Image.open("marvel-heros-villains-reduced.3.jpg")
imageHide = Image.open("diamond-shaped-texture-background.jpg")
       
for i in range(0, 50):
    y = i / 10
    x = i % 10
    x1 = x * xdim 
    x2 = x1 + xdim
    y1 = int(y * (293.0 / 5.0))
    y2 = y1 + ydim
    
    images.append(imageFace.crop((x1, y1, x2, y2)))

images.append(imageHide.crop((0, 0, xdim, ydim)))

print "working!"

for i in range(0, len(images) - 1):
    print "file %d " % i
    images[i].save("hero_%d.ppm" % i)

print "file 50"
images[50].save("hide_card.ppm")

print "done!"

El resultat de l’script anterior són les imatges amb les cares dels herois/malvats.

He utilitzat una aproximació basada en objectes. He assignat una classe a cada un dels objectes principals, i he posat cada classe en un fitxer, a més d’un fitxer memory_main.py principal des d’on es llença l’execució de l’aplicació.

Els objectes i els scripts corresponents són :

memory_main.py           --> incia el joc

memory_model.py          -->  class MemoryModel
memory_card.py           -->  class ImageButton
memory_controller.py     -->  class MemoryController

memory_app_window.py     -->  class MemoryAppWindow
memory_frame_new_game.py -->  class FrameNewGame
memory_frame_memory.py   -->  class FrameMemory
memory_frame_congrat.py  -->  class FrameCongrat

El joc s’inicia amb memory_main que executa el constructor de MemoryAppWindow

El constructor de MemoryAppWindow carrega la finestra principal i inicialitza tauler buit, llista de cartes, controlador del joc que es compartirà amb tots els frames i inicialitza els frames. A continuació mostra el frame newGame invocant-ne el constructor, al que li passa el controlador i la llista de frames

Des del frame de nou joc es pot triar la mida del tauler. EN fer click a una mida, passa o obrir-se el tauler amb la mida sol·licitada, per a fer-ho cal carregar el tauler amb cartes, que en aquest cas, són objectes de la classe ImageButton, que no és més que un embolcall del Button de Tkinter amb una PhotoImage. El cor de FrameMemory és un array de ImageButtons que es carrega al controlador. Es necessari que estigui allà perquè és al controlador on està la lògica d’anàlisi de la jugada i on es decideix si la parella queda descoberta, o es tapa, o si finalitza el joc.

Quan s’ha destapat totes les cartes, es mostra el frame de felicitacions i presentació el nombre de clicks. per iniciar un altre joc cal triar l’opció de nou joc al menú, o bé l’opció d’acabar si no es vol seguir.

Només comentar que, a diferència d’altres programes amb Tkinter que he fet, en aquest les classes Frame no deriven del Frame de Tkinter, si no que tenen el frame com un atribut. Seria una altre opció de codificació.

Com que he partit de la versió en modetext que ha seguit un paradigma de programació estructurada, el resultat és que aquest model d’objectes ja vingut donat per una redistribució del codi més que no pas per un anàlisi d’objectes pròpiament dit, a més que Tkinter imposa algunes característiques,com la necessitat d’un mainloop.

En tot cas, el joc queda força bonic, i la creació de la GUI amb Tkinter ha estat força directa.

Per si voleu fer un cop d’ull al codi, el podeu trobar al respositori GitHub : https://github.com/abaranguer/memory

Per acabar, un tauler de 10×10 amb una partida a punt d’acabar :

Caçar el Wumpus!

huntthewumpus

“Hunt the Wumpus” és un joc d’ordinador de 1972, inicialment escrit en mode text i desenvolupat en llenguatge BASIC. La seva simplicitat, a més de tenir les fonts disponibles, va motivar l’aparició de diverses i successives versions.

En aquest post escric la meva pròpia versió en català i en Python

Vet aquí les instruccions de “Caçar el Wumpus” :

Caçar el Wumpus
—————
El Wumpus és un monstre enorme i pesat amb la pell plena de ventoses
que s’alimenta de tot allò que cau a les seves urpes.
Tu ets un caçador que vol aconseguir el cap del Wumpus com a trofeu
i ets a un castell on se sap que n’hi ha un.
El castell té vint habitacions, cada habitació es connecta amb
altres tres habitacions.
Si entres a l’habitació on és el Wumpus, et menjarà.
Al castell hi ha un parell d’habitacions que tenen un fals terra.
Només entrar s’ensorra el terra i caus a un pou amb un potent àcid al fons.
Tothom que entra a una habitació amb pou, mor.
Menys el Wumpus. El Wumpus no hi cau perquè amb les seves ventoses
s’enganxa a les parets i no toca l’àcid.
Al castell també hi ha un parell de rats penats gegants.
Si entres a una habitació amb un rat penat, t’agafarà, se t’emportarà volant
i et deixarà caure a una habitació qualsevol del castell.
Els rats penats no poden endur-se al Wumpus perquè pesa massa per ells
i el Wumpus no es pot menjar als rats penats perquè són massa ràpids
per atrapar-los.
Saps que a alguna de les habitacions adjacents hi ha el Wumpus
perquè mai s’ha banyat i fa molta pudor, i el pots ensumar.
Saps que a les habitacions adjacents hi poden haver rats-penats
perquè sents el soroll del batec de les seves ales,
I, finalment, saps que a les habitacions adjacents poden haver pous
perquè sents el soroll de l’acid bullint.
Tens cinc fletxes. Has de moure’t pel castell
i quan dedueixis a quina habitació està el Wumpus, pots tirar-li
una fletxa des d’una de les habitacions adjacents.
Si l’habitació a la que has tirat la fletxa és la del Wumpus,
el mataràs i hauràs guanyat.
Però si el Wumpus no és a l’habitació, el soroll de la fletxa
potser el posarà en alerta i farà que es mogui a alguna habitació
contigua a la que ocupa.
Si esgotes totes les fletxes, aleshores ja no tens defensa
possible contra el Wumpus, que et trobarà i et menjarà.

Com he implementat les anteriors instruccions?

“El castell té vint habitacions, cada habitació es connecta amb altres tres habitacions.”

De fet, el castell del wumpus es pot modelar com un dodecaedre, on cada vèrtex representa una habitació i cada aresta ens porta a l’habitació contigua.

Vet aquí una bonica imatge d’un dodecaedre regular extreta de la Viquipèdia:

Si aplano aquesta figura obtinc una versió del mapa que em serà més útil.

Per a dibuixar el mapa del castell he fet servir el següent codi Python :

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

import Tkinter as tk
import math

class Room:
    xc = 0
    yc = 0
    x0 = 0
    y0 = 0
    x1 = 0
    y1 = 0
    doors = []

if __name__ == "__main__":
    # initialize map
    rooms = [Room() for i in range(0, 20)]

    rooms[ 0].doors = [1, 4, 5]
    rooms[ 1].doors = [0, 2, 7]
    rooms[ 2].doors = [1, 3, 9]
    rooms[ 3].doors = [2, 4, 11]
    rooms[ 4].doors = [0, 3, 13]
    rooms[ 5].doors = [0, 6, 14]
    rooms[ 6].doors = [5, 7, 16]
    rooms[ 7].doors = [1, 6, 8]
    rooms[ 8].doors = [7, 9, 17]
    rooms[ 9].doors = [2, 8, 10]
    rooms[10].doors = [9, 11, 18]
    rooms[11].doors = [3, 10, 12]
    rooms[12].doors = [11, 13, 19]
    rooms[13].doors = [4, 12, 14]
    rooms[14].doors = [5, 13, 15]
    rooms[15].doors = [14, 16, 19]
    rooms[16].doors = [6, 15, 17]
    rooms[17].doors = [8, 16, 18]
    rooms[18].doors = [10, 17, 19]
    rooms[19].doors = [12, 15, 18]    
    
    root = tk.Tk()
    canvas = tk.Canvas(root, width=600, height=600, borderwidth=0, highlightthickness=0, bg="grey")
    j = 0
    R = 20
    for i in range(0, 5):
        x = 300 + int(80.0 * math.cos( (2.0 * math.pi / 5.0) * i)) 
        y = 300 + int(80.0 * math.sin( (2.0 * math.pi / 5.0) * i))

        rooms[j].xc = x
        rooms[j].yc = y
        rooms[j].x0 = x - R
        rooms[j].y0 = y - R
        rooms[j].x1 = x + R
        rooms[j].y1 = y + R
        j = j + 1

    for i in range(0, 10):
        x = 300 + int(180.0 * math.cos( (math.pi / 5.0) * i)) 
        y = 300 + int(180.0 * math.sin( (math.pi / 5.0) * i))

        rooms[j].xc = x
        rooms[j].yc = y
        rooms[j].x0 = x - R
        rooms[j].y0 = y - R
        rooms[j].x1 = x + R
        rooms[j].y1 = y + R
        j = j + 1

    for i in range(0, 5):
        x = 300 + int(280.0 * math.cos( (2.0 * math.pi / 5.0) * i - (math.pi / 5.0)))
        y = 300 + int(280.0 * math.sin( (2.0 * math.pi / 5.0) * i - (math.pi / 5.0)))

        rooms[j].xc = x
        rooms[j].yc = y
        rooms[j].x0 = x - R
        rooms[j].y0 = y - R
        rooms[j].x1 = x + R
        rooms[j].y1 = y + R
        j = j + 1

    for i in range(0, 20):
        canvas.create_line(rooms[i].xc, \
                           rooms[i].yc, \
                           rooms[rooms[i].doors[0]].xc, \
                           rooms[rooms[i].doors[0]].yc)

        canvas.create_line(rooms[i].xc, \
                           rooms[i].yc, \
                           rooms[rooms[i].doors[1]].xc, \
                           rooms[rooms[i].doors[1]].yc)
        
        canvas.create_line(rooms[i].xc, \
                           rooms[i].yc, \
                           rooms[rooms[i].doors[2]].xc, \
                           rooms[rooms[i].doors[2]].yc)

    for i in range(0, 20):
        canvas.create_oval(rooms[i].x0, rooms[i].y0, rooms[i].x1, rooms[i].y1, fill='white')
        canvas.create_text(rooms[i].xc, rooms[i].yc, text='%d' % i, fill='black')
        
    canvas.grid()
    root.wm_title("El Castell del Wumpus!")
    root.mainloop()

Destacar en el codi anterior l’us del canvas de Tkinter per a fer el dibuix, i l’us de la Python comprehension per a inicialitzar l’array rooms (just abans d’assignar les door de cada room).

Al codi anterior ja apareix l’estructura del mapa : el array rooms, de 20 posicions, on cada posició conté una instancia de la classe Room que, entre d’altres propietats, té l’array doors de tres posicions, que manté els números de les habitacions que es connecten a l’habitació.

“Al castell hi ha un parell d’habitacions que tenen un fals terra…
Al castell també hi ha un parell de rats penats gegants…
Tens cinc fletxes…·

Per tant, tenim els següents personatges o objectes del joc : un caçador, un wumpus, dos pous d’àcid, dos rats penats gegants i cinc fletxes. Les cinc fletxes les porta el caçador i no cal, doncs, ubicar-les específicament. El que cal fer en iniciar el joc és col·locar els objectes wumpus, caçador, pous i rats penats al castell, de forma que el caçador no coincideix amb el wumpus, els rats penats i els pous; els rats penats no coincideixen entre ells ; i els pous tampoc. És dir : el wumpus podria compartir habitació amb un rat-penat i un pou. Però això és possible perquè “El wumpus no hi cau [als pous] perquè amb les seves ventoses s’enganxa a les parets i en pot sortir sense perill.” i “Els rats penats no poden endur-se al wumpus perquè pesa massa per ells i el wumpus no es pot menjar als rats penats perquè són massa ràpids per ell.”.

Això es fa amb aquest codi :

Una classe molt simple GameObject, que manté la ubicació de l’objecte

class GameObject:
    roomNumber = -1

    def __init__(self, roomNumber):
        self.roomNumber = roomNumber
    # initialize characters and objects
    wumpusRoom = -1
    bat1Room = -1
    bat2Room = -1
    pit1Room = -1
    pit2Room = -1
    hunterRoom = -1
    arrows = 5

    while (hunterRoom == wumpusRoom) or \
          (hunterRoom == bat1Room) or   \
          (hunterRoom == bat2Room) or   \
          (hunterRoom == pit1Room) or   \
          (hunterRoom == pit2Room) or   \
          (pit1Room == pit2Room) or     \
          (bat1Room == bat2Room):
        
        hunterRoom = rnd.randint(0, 19)
        wumpusRoom = rnd.randint(0, 19)
        bat1Room = rnd.randint(0, 19)
        bat2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)

    hunter = GameObject(hunterRoom)
    wumpus = GameObject(wumpusRoom)
    bat1 = GameObject(bat1Room)
    bat2 = GameObject(bat2Room)
    pit1 = GameObject(pit1Room)
    pit2 = GameObject(pit2Room)

    return {"rooms":rooms, "hunter":hunter, "wumpus":wumpus,
            "bat1":bat1, "bat2":bat2, "pit1":pit1, "pit2":pit2, "arrows":arrows}

L’estratègia utilitzada és repetir fins que es torna una combinació a l’atzar que ubica tots els objectes en habitacions diferents.

Destacar, també, l’us d’un diccionari per retornar els objectes.

“Si entres a l’habitació on és el Wumpus, et menjarà…
Tothom que hi cau [a una habitació amb pou], mor…
Si entres a una habitació amb un rat penat, t’agafarà, se t’emportarà volant
i et deixarà caure a una habitació qualsevol del castell…

A la funció enterRoom tenim la codificació de les regles anteriors

def enterRoom(gameObjects):
    continueGame = True
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    
    print "\nEts a l'habitació número %d" % hunterRoom

    if wumpusRoom == hunterRoom:
        print "El wumpus és a l'habitació!"
        print "T'ha atrapat i comença a devorar-te!" 
        print "No és que sigui dolent... Et menja perquè és la seva natura de Wumpus..."    
        continueGame = False

    if continueGame and (hunterRoom in (pit1Room, pit2Room)):
        print "Hi ha un pou amb acid a l'habitació!"
        print "Has caigut dins i comences a dissoldre't de forma lenta i dolorosament agònica!"
        continueGame = False

    if continueGame and (hunterRoom in (bat1Room, bat2Room)):
        print "I un rat penat gegant a l'habitació! T'ha agafat i se t'emporta volant!"
        newHunterRoom = rnd.randint(0, 19)
        newBat1Room = rnd.randint(0, 19)
        newBat2Room = rnd.randint(0, 19)
        gameObjects.get("hunter").roomNumber = newHunterRoom
        gameObjects.get("bat1").roomNumber = newBat1Room
        gameObjects.get("bat2").roomNumber = newBat2Room
        print "El rat penat t'ha deixat caure a l'habitació número %d" % newHunterRoom
        continueGame = enterRoom(gameObjects)

    if not continueGame:
        print "Has mort de forma horrible!" 
        print "..." 
        print "Però altres vindran a intentar triomfar allà on tu has fracassat tan lamentablement "

    return continueGame


“Has de moure’t pel castell…”

Una de les possibles accions és, doncs, moure’s a una habitació adjacent.

El següent codi és la part de la funció actionHunter que recull l’acció “moure’s” del caçador :

def actionsHunter(gameObjects):
    continueGame = True
    wumpusMove = False
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    arrows = gameObjects.get("arrows")
    rooms = gameObjects.get("rooms")
    [door0, door1, door2] = rooms[hunterRoom].doors

    while True:
        action = raw_input("Moure's 'm' o Tirar una fletxa 's' ? ")
        if action in ('m','s','M','S'):
            break

    while True:
        try:
            door = int(raw_input("A quina habitació %d, %d or %d ? " % (door0, door1, door2)))
            if door in (door0, door1, door2):
              break
        except:
            None

    if (action=='m' or action=='M'):
        gameObjects.get("hunter").roomNumber = door


“quan dedueixis a quina habitació està el Wumpus, pots tirar-li una fletxa des d’una de les habitacions adjacents.

Si l’habitació a la que has tirat la fletxa és la del wumpus, el mataràs i hauràs guanyat.

Però si el Wumpus no és a l’habitació, el soroll de la fletxa potser el posarà en alerta i farà que es mogui a alguna habitació contigua a la que ocupa…”

Això és el que es fa amb el següent fragment de actionsHunter

    if ((action=='s' or action=='S') and \
        ((wumpusRoom == door0) or \
         (wumpusRoom == door1) or \
         (wumpusRoom == door2))):
            print "La fletxa ha matat al Wumpus!"
            print "La protectora d'animals et denunciarà si et descobreixen!"
            print "Ets l'assassí del wumpus!"
            print "Un altre sàdic que mata pobres bestioletes indefenses!"
            print "Suposo que estaràs content, criminal!"
            print "En fi. Suposo que t'he de felicitar perquè has guanyat."
            continueGame = False

    if ((action=='s' or action=='S') and \
        ((wumpusRoom <> door0) and \
         (wumpusRoom <> door1) and \
         (wumpusRoom <> door2))):
            print "has fallat! No hi havia cap wumpus a l'habitació!"

            # move wumpus
            doorsWumpus = doors[wumpusRoom].doors
            randomDoor = rnd.random()
            if randomDoor < 0.75 :                 wumpusMove= True             if randomDoor >= 0 and randomDoor < 0.25:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[0]             if randomDoor >= 0.25 and randomDoor < 0.50:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[1]             if randomDoor >= 0.50 and randomDoor < 0.75:
                gameObjects.get("wumpus").roomNumber = doorsWumpus[2]


Si esgotes totes les fletxes, aleshores ja no tens defensa possible contra el wumpus, que et trobarà i et menjarà.

És la part final d’actionsHunter

            arrows = arrows - 1

            if arrow == 1:
                aux = "fletxa"
            else:
                aux = "fletxes"
            print "Ara tens %d %s" % (arrows, aux)

            if arrow == 0:
                print "No tens fletxes i, per tant, ja no pots matar el Wumpus."
                print "No trigarà a descobrir-te i, aleshores, et menjarà."
                print "Serà molt dolorós i horrible..."
                print "Ja se sent com ve!"
                print "Ara entra per la porta i es llença sobre teu!"
                print "La resistència és inútil.."
                print "Encara ets conscient quan se t'empassa!"
                print "Els àcids de la seva digestio fan la resta. Has mort."
                print "Però recorda que no és que el wumpus sigui dolent..." 
                print "Et menja perquè és la seva natura de Wumpus..."
            continueGame = False

            if continueGame and wumpusMove:
                print "La fletxa ha alertat el Wumpus i es mou! espero que no vingui cap aquí!"

“Saps que a alguna de les habitacions adjacents hi ha el Wumpus perquè mai s’ha banyat i fa molta pudor, i el pots ensumar.

Saps que a les habitacions adjacents hi poden haver rats-penats
perquè sents el soroll del batec de les seves ales.

I, finalment, saps que a les habitacions adjacents poden haver pous
perquè sents el soroll de l’acid bullint.”

Si el caçador pot entrar a una habitació, pot fer-se una idea de que es trobarà més endavant. És el que s’aconsegueix amb la funció examineRoom.

def examineRoom(gameObjects):
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    arrows = gameObjects.get("arrows")
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    [door0, door1, door2] = rooms[hunterRoom].doors
    
    print "L'habitació %d es connecta amb les habitacions %d, %d i %d" % (hunterRoom, door0, door1, door2)

    if arrows > 1:
        aux = "fletxes"
    else:
        aux = "fletxa"
    print "Encara tens %d %s" % (arrows, aux) 
    
    if wumpusRoom in rooms[hunterRoom].doors:
        print "Fa una insuportable fortor de wumpus!"
        print "Hi ha un wumpus pudent a una de les habitacions que es connecten des d'aquí!"

    if (bat1Room in rooms[hunterRoom].doors) or (bat2Room in rooms[hunterRoom].doors):
        print "Se sent soroll d'ales!"
        print "Hi ha almenys un rat penat gegant a les habitacions que es connecten des d'aquí"

    if (pit1Room in rooms[hunterRoom].doors) or (pit2Room in rooms[hunterRoom].doors):
        print "Se sent el soroll d'àcid bullint!"
        print "Hi ha almenys un pou ple d'àcid a les habitacions que es connecten des d'aquí"

En aquest moment, ja tinc tots els elements. Si ho junto tot el que obtinc és el següent codi :

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

import random as rnd

class Room:
    doors = []

class GameObject:
    roomNumber = -1

    def __init__(self, roomNumber):
        self.roomNumber = roomNumber

def initializeObjects():
    # map
    rooms = [Room() for i in range(0, 20)]

    # initialize map
    rooms[ 0].doors = [1, 4, 5]
    rooms[ 1].doors = [0, 2, 7]
    rooms[ 2].doors = [1, 3, 9]
    rooms[ 3].doors = [2, 4, 11]
    rooms[ 4].doors = [0, 3, 13]
    rooms[ 5].doors = [0, 6, 14]
    rooms[ 6].doors = [5, 7, 16]
    rooms[ 7].doors = [1, 6, 8]
    rooms[ 8].doors = [7, 9, 17]
    rooms[ 9].doors = [2, 8, 10]
    rooms[10].doors = [9, 11, 18]
    rooms[11].doors = [3, 10, 12]
    rooms[12].doors = [11, 13, 19]
    rooms[13].doors = [4, 12, 14]
    rooms[14].doors = [5, 13, 15]
    rooms[15].doors = [14, 16, 19]
    rooms[16].doors = [6, 15, 17]
    rooms[17].doors = [8, 16, 18]
    rooms[18].doors = [10, 17, 19]
    rooms[19].doors = [12, 15, 18]

    # initialize characters and objects
    wumpusRoom = -1
    bat1Room = -1
    bat2Room = -1
    pit1Room = -1
    pit2Room = -1
    hunterRoom = -1
    arrows = 5

    while (hunterRoom == wumpusRoom) or \
          (hunterRoom == bat1Room) or   \
          (hunterRoom == bat2Room) or   \
          (hunterRoom == pit1Room) or   \
          (hunterRoom == pit2Room) or   \
          (pit1Room == pit2Room) or     \
          (bat1Room == bat2Room):
        
        hunterRoom = rnd.randint(0, 19)
        wumpusRoom = rnd.randint(0, 19)
        bat1Room = rnd.randint(0, 19)
        bat2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)
        pit2Room = rnd.randint(0, 19)

    hunter = GameObject(hunterRoom)
    wumpus = GameObject(wumpusRoom)
    bat1 = GameObject(bat1Room)
    bat2 = GameObject(bat2Room)
    pit1 = GameObject(pit1Room)
    pit2 = GameObject(pit2Room)

    return {"rooms":rooms, "hunter":hunter, "wumpus":wumpus,
            "bat1":bat1, "bat2":bat2, "pit1":pit1, "pit2":pit2, "arrows":arrows}

def enterRoom(gameObjects):
    continueGame = True
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    
    print "\nEts a l'habitació número %d" % hunterRoom

    if wumpusRoom == hunterRoom:
        print "El wumpus és a l'habitació!"
        print "T'ha atrapat i comença a devorar-te!" 
        print "No és que sigui dolent... Et menja perquè és la seva natura de Wumpus..."    
        continueGame = False

    if continueGame and (hunterRoom in (pit1Room, pit2Room)):
        print "Hi ha un pou amb acid a l'habitació!"
        print "Has caigut dins i comences a dissoldre't de forma lenta i dolorosament agònica!"
        continueGame = False

    if continueGame and (hunterRoom in (bat1Room, bat2Room)):
        print "I un rat penat gegant a l'habitació! T'ha agafat i se t'emporta volant!"
        newHunterRoom = rnd.randint(0, 19)
        newBat1Room = rnd.randint(0, 19)
        newBat2Room = rnd.randint(0, 19)
        gameObjects.get("hunter").roomNumber = newHunterRoom
        gameObjects.get("bat1").roomNumber = newBat1Room
        gameObjects.get("bat2").roomNumber = newBat2Room
        print "El rat penat t'ha deixat caure a l'habitació número %d" % newHunterRoom
        continueGame = enterRoom(gameObjects)

    if not continueGame:
        print "Has mort de forma horrible!" 
        print "..." 
        print "Però altres vindran a intentar triomfar allà on tu has fracassat tan lamentablement "

    return continueGame

def examineRoom(gameObjects):
    rooms = gameObjects.get("rooms")
    hunterRoom = gameObjects.get("hunter").roomNumber
    arrows = gameObjects.get("arrows")
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    bat1Room = gameObjects.get("bat1").roomNumber
    bat2Room = gameObjects.get("bat2").roomNumber
    pit1Room = gameObjects.get("pit1").roomNumber
    pit2Room = gameObjects.get("pit2").roomNumber
    [door0, door1, door2] = rooms[hunterRoom].doors
    
    print "L'habitació %d es connecta amb les habitacions %d, %d i %d" % (hunterRoom, door0, door1, door2)

    if arrows > 1:
        aux = "fletxes"
    else:
        aux = "fletxa"
    print "Encara tens %d %s" % (arrows, aux) 
    
    if wumpusRoom in rooms[hunterRoom].doors:
        print "Fa una insuportable fortor de wumpus!"
        print "Hi ha un wumpus pudent a una de les habitacions que es connecten des d'aquí!"

    if (bat1Room in rooms[hunterRoom].doors) or (bat2Room in rooms[hunterRoom].doors):
        print "Se sent soroll d'ales!"
        print "Hi ha almenys un rat penat gegant a les habitacions que es connecten des d'aquí"

    if (pit1Room in rooms[hunterRoom].doors) or (pit2Room in rooms[hunterRoom].doors):
        print "Se sent el soroll d'àcid bullint!"
        print "Hi ha almenys un pou ple d'àcid a les habitacions que es connecten des d'aquí"
    
def actionsHunter(gameObjects):
    continueGame = True
    wumpusMove = False
    hunterRoom = gameObjects.get("hunter").roomNumber
    wumpusRoom = gameObjects.get("wumpus").roomNumber
    arrows = gameObjects.get("arrows")
    rooms = gameObjects.get("rooms")
    [door0, door1, door2] = rooms[hunterRoom].doors

    while True:
        action = raw_input("Moure's 'm' o Tirar una fletxa 's' ? ")
        if action in ('m','s','M','S'):
            break

    while True:
        try:
            door = int(raw_input("A quina habitació %d, %d or %d ? " % (door0, door1, door2)))
            if door in (door0, door1, door2):
              break
        except:
            None

    if (action=='m' or action=='M'):
        gameObjects.get("hunter").roomNumber = door

    if ((action=='s' or action=='S') and \
        ((wumpusRoom == door0) or \
         (wumpusRoom == door1) or \
         (wumpusRoom == door2))):
            print "La fletxa ha matat al Wumpus!"
            print "La protectora d'animals et denunciarà si et descobreixen!"
            print "Ets l'assassí del wumpus!"
            print "Un altre sàdic que mata pobres bestioletes indefenses!"
            print "Suposo que estaràs content, criminal!"
            print "En fi. Suposo que t'he de felicitar perquè has guanyat."
            continueGame = False

    if ((action=='s' or action=='S') and \
        ((wumpusRoom <> door0) and \
         (wumpusRoom <> door1) and \
         (wumpusRoom <> door2))):
            print "has fallat! No hi havia cap wumpus a l'habitació!"

            # move wumpus
            doorsWumpus = doors[wumpusRoom].doors
            randomDoor = rnd.random()
            if randomDoor < 0.75 :                 wumpusMove= True             if randomDoor >= 0 and randomDoor < 0.25:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[0]             if randomDoor >= 0.25 and randomDoor < 0.50:                 gameObjects.get("wumpus").roomNumber = doorsWumpus[1]             if randomDoor >= 0.50 and randomDoor < 0.75:
                gameObjects.get("wumpus").roomNumber = doorsWumpus[2]

            arrows = arrows - 1

            if arrow == 1:
                aux = "fletxa"
            else:
                aux = "fletxes"
            print "Ara tens %d %s" % (arrows, aux)

            if arrow == 0:
                print "No tens fletxes i, per tant, ja no pots matar el Wumpus."
                print "No trigarà a descobrir-te i, aleshores, et menjarà."
                print "Serà molt dolorós i horrible..."
                print "Ja se sent com ve!"
                print "Ara entra per la porta i es llença sobre teu!"
                print "La resistència és inútil.."
                print "Encara ets conscient quan se t'empassa!"
                print "Els àcids de la seva digestio fan la resta. Has mort."
                print "Però recorda que no és que el wumpus sigui dolent..." 
                print "Et menja perquè és la seva natura de Wumpus..."
            continueGame = False

            if continueGame and wumpusMove:
                print "La fletxa ha alertat el Wumpus i es mou! espero que no vingui cap aquí!"

    return continueGame

def gameLoop(gameObjects):
    continueGame = True

    while continueGame:
        continueGame = enterRoom(gameObjects)
        if continueGame:
            examineRoom(gameObjects)
            continueGame = actionsHunter(gameObjects)

instructions = '''
Caçar el Wumpus
---------------
El Wumpus és un monstre enorme i pesat amb la pell plena de ventoses 
que s'alimenta de tot allò que cau a les seves urpes. 
Tu ets un caçador que vol aconseguir el cap del Wumpus com a trofeu 
i ets a un castell on se sap que n'hi ha un.  
El castell té vint habitacions, cada habitació es connecta amb 
altres tres habitacions.
Si entres a l'habitació on és el Wumpus, et menjarà.
Al castell hi ha un parell d'habitacions que tenen un fals terra. 
Només entrar s'ensorra el terra i caus a un pou amb un potent àcid al fons. 
Tothom que entra a una habitació amb pou, mor. 
Menys el Wumpus. El Wumpus no hi cau perquè amb les seves ventoses 
s'enganxa a les parets i no toca l'àcid.
Al castell també hi ha un parell de rats penats gegants. 
Si entres a una habitació amb un rat penat, t'agafarà, se t'emportarà volant 
i et deixarà caure a una habitació qualsevol del castell. 
Els rats penats no poden endur-se al Wumpus perquè pesa massa per ells 
i el Wumpus no es pot menjar als rats penats perquè són massa ràpids 
per atrapar-los.
Saps que a alguna de les habitacions adjacents hi ha el Wumpus
perquè mai s'ha banyat i fa molta pudor, i el pots ensumar.
Saps que a les habitacions adjacents hi poden haver rats-penats
perquè sents el soroll del batec de les seves ales,
I, finalment, saps que a les habitacions adjacents poden haver pous 
perquè sents el soroll de l'acid bullint.
Tens cinc fletxes. Has de moure't pel castell 
i quan dedueixis a quina habitació està el Wumpus, pots tirar-li 
una fletxa des d'una de les habitacions adjacents. 
Si l'habitació a la que has tirat la fletxa és la del Wumpus, 
el mataràs i hauràs guanyat. 
Però si el Wumpus no és a l'habitació, el soroll de la fletxa 
potser el posarà en alerta i farà que es mogui a alguna habitació 
contigua a la que ocupa.
Si esgotes totes les fletxes, aleshores ja no tens defensa 
possible contra el Wumpus, que et trobarà i et menjarà.
'''

if __name__ == "__main__":

    print instructions

    # gameObjects = [rooms, hunter, wumpus, bat1, bat2, pit1, pit2]
    gameObjects = initializeObjects()
    
    gameLoop(gameObjects)

Per si algú té ganes de millorar-lo -afegint una interfície gràfica, per exemple- el codi es pot descarregar del repositori GitHub.