Mostrando entradas con la etiqueta programming. Mostrar todas las entradas
Mostrando entradas con la etiqueta programming. Mostrar todas las entradas

2023/08/03

Emulador chip-8

 

En el marco cyberciruja, gzalo dictó un excelente taller de emulación de CHIP-8 el sábado 2023-07-22. Comparto mi experiencia y resultado.


Me apoyé en el ejemplo provisto, tanto el base como los fragmentos de código de la presentación, le refactoricé las globales a un struct, quité el boolean y renombré .cpp a .c, es que había una trampita de estar usando g++ para compilar código C.

 

Screen dump al finalizar la ejecución
Screen dump al finalizar la ejecución

 

Mi objetivo, además de la diversión, es repasar un poco C para no olvidar demasiado, probar algunos recursos que no he usado mucho y otros que sí pero no en C. Entonces ejercité:

  • Makefile 
  • procesar argumentos con getopt
  • FSM
  • testing unitario sin framework
  • valgrind
  • gdb

Mi enfoque fue incrementar de modo testeable la menor funcionalidad posible en cada paso. En un comienzo, testeable era que ejecutara el programa hasta la próxima instrucción no implementada, luego agregué test unitario.

Antes de seguir, para no spoilearte, te dejo mi recomendación de cómo encararlo:

  • Elegí e inicializá las estructuras u objetos de tu arquitectura.
  • Hacé el loop de ejecución.
  • Implementá los dumps de los registros y estructuras
  • Cargá la ROM
  • Armá el switch para los opcodes y los switches internos para los subcodes, poné en cada rama una llamada a una función unimplemented(), unknown() en el default del switch principal, invalid() en cada default de los switches internos:


     void unimplemented(){
       printf("UNIMPLEMENTED INSTRUCTION\n");
       exit(1);
     }

  • Ejecutá el programa, tiene que tirar un unimplemented, implementá esa instrucción. Y así....

Ya sea a mano o con un framework de testing, respetá TDD.

  • Ejecutá hasta el unimplemented: eso te genera el requerimiento
  • Escribí el test ateniendote a la especificación, ojo que hay sutiles diferencias
    • https://chip8.gulrak.net/ (marcá solo CHIP-8) 
    • https://github.com/Timendus/chip8-test-suite
    • https://en.wikipedia.org/wiki/CHIP-8
    • https://tobiasvl.github.io/blog/write-a-chip-8-emulator/
  • Dejá que falle para comprobar que es efectivo en la falla
  • Hardcodeá para comprobar que es efectivo en el éxito
  • Implementá hasta que pase
  • Refactorizá
  • Again...

 

Spoiler Alert 


Ahora te cuento lo que hice yo, que más o menos se parece, está en cpantel/CHIP-8

Implementé la carga del programa en memoria y comprobé visualmente que soc_dump_memory() y hexdump -C ROM/1-chip8-logo.ch8 correspondieran. Recordá que en el dump debe estar la fuente, luego todos ceros y la imagen del ROM recién en 0x200, 512 decimal)

Comparación de memory dump y hexdump de 3-corax+.ch8
Comparación de memory dump y hexdump de 3-corax+.ch8

Implementé  avanzar un paso, que haga el fetch y la descomposición en opcode, nibbles, X, Y, keys, NN, NNN, el nombre que sea útil en cada contexto

Tomé el ROM 1-chip8-logo.ch8 que es el que menos instrucciones precisa. De cada instrucción que invoca, implementé el decode y execute. Obviamente para SPRITE me copié de lo que Gustavo proveyó.

Para la primera instrucción que es CLS, mi clear_screen toma un valor, así en init() pongo la pantalla en blanco y ante la primera llamada de CLS se pone en negro.

Implemente que varias medidas intrusivas se activen con left shift, tales como:

  • avance paso a paso
  • volcado de memoria, video, registros, teclas
  • des/activación de debug

 Esto es por que el teclado esta mapeado en:

  1 2 3 4

   q w r t

    a s d f

     z x c v

Y algunas de esas letras son buenas como atajos:

while (!quit) {
  while (SDL_PollEvent(&event)) {
    if (event.type == SDL_QUIT) {
      quit = 1;
    } else if(event.type == SDL_KEYDOWN){
      if (event.key.keysym.mod & KMOD_LSHIFT ) {
 
      // para que se vea bien acá
        // sym = event.key.keysym.sym;
        if(sym == SDLK_r) run = ! run;
        if(sym == SDLK_n) soc_step(&soc,1);
        if(sym == SDLK_d) {debug = ! debug;         run=0;}
        if(sym == SDLK_t) {show_time = ! show_time; run=0;}
        if(sym == SDLK_v) {soc_dump_registers(&soc);run=0;}
        if(sym == SDLK_m) {soc_dump_memory(&soc);   run=0;}
        if(sym == SDLK_s) {soc_dump_screen(&soc);   run=0;}
        if(sym == SDLK_k) {soc_dump_key(&soc);      run=0;}
        if(sym == SDLK_h) {show_help();             run=0;}
        if(sym == SDLK_q) quit = 1;
      } else {
        if(sym == SDLK_1) soc_press_key(&soc, 1);
        ....

      }
    } else if(event.type == SDL_KEYUP){
      if (event.key.keysym.mod == KMOD_NONE ) {
        if(sym == SDLK_1) soc_release_key(&soc, 1);
        ...

    if (run) soc_step(&soc, debug);
  }
  ...


Ahí entrás en un loop de ir implementando la siguiente instrucción que falle....


pc: 200 fetch: 00E0 opcode: 0 X: 0 Y: E opcode2: 0 NN: E0 NNN: 0E0
pc: 202 fetch: A22A 
opcode: A X: 2 Y: 2 opcode2: A NN: 2A NNN: 22A
pc: 204 fetch: 600C 
opcode: 6 X: 0 Y: 0 opcode2: C NN: 0C NNN: 00C
pc: 206 fetch: 6108 
opcode: 6 X: 1 Y: 0 opcode2: 8 NN: 08 NNN: 108
pc: 208 fetch: D01F 
opcode: D X: 0 Y: 1 opcode2: F NN: 1F NNN: 01F
UNIMPLEMENTED INSTRUCTION


Implementé una opciones de línea de comando para determinar:

  • Estado inicial variable
    • Corriendo o pausado
    • Imprimiendo debug o no
    • Hacer o no dump de pantalla al finalizar
  • Cantidad de instrucciones a ejecutar hasta detenerse
  • Cantidad de instrucciones por frame, 8 parece ok
  • Delay para completar el frame, medio en vano, todo ocurre en mucho menos de un milisegundo, el default de 16 parece ok
  • ROM a cargar

Fui tomando cada ROM de https://github.com/Timendus/chip8-test-suite y no hubo problemas hasta la quinta, quirks. De paso ahí noté que muchas de las operaciones tenían una equis en lugar del check ok en los anteriores, entonces tuve que...

 

Test

 

Llegó el momento de agregar test unitario. Hasta este momento tenía algo parecido a:

void step(struct typeSOC* soc) {
  struct typeInstruction ins;
  fetch(soc,&ins);
  soc->pc+=2;
  switch (ins.opcode) {
    case 0x3:
      if (soc->v[ins->X] == ins->NN) soc->pc+=2;
    break;
    ...
  ...

Lo cual es imposible de probar, la implementación de la instrucción debe estar en una función aislada para no estar probando a la vez el fetch y fundamentalmente ese soc->pc+=2;

Me llevé la implementación de cada operación a otro archivo

  switch (ins.opcode) {
    case 0x0: // CLS
      switch(ins.NNN) {
        case 0x0E0:
          api(soc, &ins, OPCODE_CLS);
          if (debug) printf("  CLS\n");
    break;

    case 0x0EE: // RET
          api(soc,&ins, OPCODE_RET);
    break;
 

 Este es el test de XOR

void test_XOR(struct typeSOC* soc, struct typeInstruction* ins) {
  soc->v[8] = 0xff;
  soc->v[2] = 0x2a;
  ins->fetch = 0x8823;
  predecode(ins);
  api(soc, ins, OPCODE_XOR);
  assert_equal(0xd5, soc->v[8], "XOR: bad result");
}

 

siendo assert_equal:

void assert_equal(uint32_t expected, uint32_t got, char * msg) {
  if (expected != got) {
    printf("\n%s, expected: %x got: %x\n", msg, expected, got);
  } else {
    printf(".");
  }
}

En lo versionado actual quedaron unos if's en lugar de assert_equal, algún dia...
 

Pude haber implementado una función para cada instrucción, tipo api_CLS(), api_RET() o como lo hice, que es pasarle a una sola función el código y ahí dentro otro switch(), lo cual puede ser mal visto por que parece haber repetición de código, pero, no estoy tan seguro. El primer switch tiene switches anidados y el segundo no, es plano. 

Usar funciones distintas es mejor manera de autodocumentar y un mejor lugar para poner los unimplemented(). Los invalid() y unknown() quedarían en el switch de soc.c.

Tal como hice los test, sin framework, tiene una muy evidente desventaja, que es no poder imprimir un puntito tras cada test realizado de modo elegante. Ni poder llevar la cuenta de los tests y asserts ejecutados. Digo de manera elegante, siempre se puede llenar todo de printfs y globales...

Otra limitación, no es que no se pueda hacer con C, es que de haber usado un lenguaje orientado a objetos, quedaría muy prolijo encapsular todo con setters y getters en lugar de acceder directamente a los registros y memorias y así poder recopilar algunas estadísticas como:

  • uso de registros
  • uso de instrucciones
  • uso del stack
  • uso de botones
  • área de memoria leída y escrita

Esto serviría para hacer una versión reducida, como para portar a FPGA chica o lo que me interesaría, el core RISC-V icicle en la EDU-FPGA.

 

Traza

 

Un recurso es analizar una traza prexistente, se puede obtener de los emuladores octo. Por ejemplo en (https://timendus.github.io/chip8-test-suite/3-corax+.html), poniendo algo como

console.log("op: " + op.toString(16) + " o: "
+ o.toString(16) + " pc: " + this.pc.toString(16));


cerca de la línea 470 cuando descompone el fetch, genera una traza. Si en tu emulador hacés algo compatible, con ./tools/compare comparás.

Esto sirve para las partes donde no hay uso de timer ni lectura de teclado.

 

Headless


Implementé una versión headless, más que todo porque en mi máquina no quería instalar SDL2, entonces hice todo en otra con ssh y una ventana de VNC, solo en esa ventana se puede ejecutar código con SDL. Medio en vano pues con ssh -X funciona casi ok.

 

Dump ascii

 

Motivado por la posibilidad de testear la imagen rederizada, implementé que se haga un dump por terminal. Para ser franco no es ascii, es unicode, pero bueno. Es la primera imagen de este post, se puede ejecutar en cualquier momento o al finalizar.

 

Autostop

 

Aunque ningún programa baremetal como son estos termina, quizás queremos que terminen al terminar de ejecutar. Esto tiene sentido en los tests y la manera de implementarlo es hacer un salto incondicional a esta misma instrucción, o sea al PC actual. Esto se detecta y finaliza la emulación.

  if (ins.opcode == 1 && soc->pc == ins.NNN) {
     return 1;
  }


Mi implementación en C

 

Está en cpantel/CHIP-8 y te cuento que no me maté mucho en que los componentes respetaran la jerarquía, fijate que debió haber sido así:
 

struct typeSOC
  pc            -> soc.cpu.pc
  i             -> soc.cpu.i
  v[]           -> soc.cpu.v[]
  stack_pointer -> soc.cpu.stack_pointer
  key[]         -> soc.keyboard.keys[]
  kb_state      -> soc.keyboard.state
  last_key      -> soc.keyborad.last 
  stack[]       -> soc.stack[]
  memory[]      -> soc.memory[]
  screen[]      -> soc.screen.memory[]
  redraw        -> soc.screen.redraw 
  delay_timer   -> soc.timer.delay
  sound_timer   -> soc.timer.sound

 
y la parte de la instruction, quizás así:
 
  count         -> soc.cpu.instruction.count
  fetch         -> soc.cpu.instruction.fetch
  opcode        -> soc.cpu.instruction.opcode
  X/key         -> soc.cpu.instruction.X/key
  Y/N/opcode2   -> soc.cpu.instruction.Y/N/opcode2
  NN            -> soc.cpu.instruction.NN
  address/NNN   -> soc.cpu.instruction.address/NNN
 
 
pero implica agregar archivos y un montón de refactorización, no vale la pena.

Valgrind lo usé para ./tools/compare, no para chip8, así que no sé si

 

Posibilidades de mejora

 

RND: proveerle una seed por línea de comando.

Comparar la memoria de pantalla con una imagen de referencia.

Poder cambiar mediante parámetros de línea los colores de background y foreground.

Que en debug imprima el código disassembly. 

Que pase el test quirks. 

Implementar las validaciones de uso del stack y algunas otras.

Que imprima algún help para las command line options.

Que valide que exista el archivo de ROM y su longitud.

Usar funciones para cada instrucción en lugar del segundo switch.

 

Conclusión

 

Muy buena la explicación de gzalo y su asistencia posterior, me destrabó al menos en un ínfimo pero mortífero error muy difícil de notar en la lectura del teclado.

La experiencia fue muy buena, sin presión, hacer algo que no sirve para nada por fuera de haberlo hecho. Es que los juegos son horribles, inusables, no probé ninguno más allá de cargarlo para ver que funcionara algo.

La mayor parte de las posibilidades de mejora serían aplicables en caso de querer desarrollar programas para esta plataforma, nada más lejos de mis intenciones, se lo dejo a la gente más retro.







2022/02/27

Ejemplo de ESP32 con lectura de DHT11

Extendiendo los pasos de primer contacto con ESP32, va tipo receta con anotaciones sin mayores explicaciones, la idea es terminar con una carpeta con el proyecto armado.

 

Instalación de dependencias

 

sudo apt install git wget flex bison gperf python3 python3-pip python3-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0


Mi entorno suele ser una virtual, por comodidad también hago:

sudo apt install vim kdiff3 dirdiff

 

Estructura de archivos

 

mkdir esp http_request

 

~/esp/esp-idf
      esp-idf-lib
      http_request

 

Aunque he puesto el ejemplo http_request a la par de los repositorios, puede estar en cualquier lado.

 

Repositorios y setup herramientas

 

El primer repo te provee un montón de dispositivos, nos interesa el componente dht11.

git clone https://github.com/UncleRus/esp-idf-lib.git

git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git

 

El segundo es la adaptación de FreeRTOS a extensa de expresif.

 

cd esp-idf

./install.sh esp32 

 

Eso te instaló lo que haga falta para ESP32. Tanto acá como en pasos posteriores, donde dice esp32 puede ir esp32c3 o esp32s2, pero por ahora sólo probé con esp32, no debería en general pero pueden haber diferencias sutiles con los pines en estos ejemplos básicos.

 

El código


cd ../http_request

. ../esp-idf/export.sh

Esto es para que funcione el entorno de build.

 

El código es copia del ejemplo http_request:

cp -r ../esp-idf/examples/protocols/http_request/ .

Ajuste del proyecto a esp32:

idf.py set-target esp32

Sería un buen momento para hacer funcionar el ejemplo así como está, pero vamos directo al ejemplo completo.

 

Dependencias de código

 

En CMakeFiles.txt, agregá la dependencia a esp-idf-lib:

set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common $ENV{IDF_PATH}/../esp-idf-lib/components)

Menuconfig

idf.py menuconfig

Si está versionando, no te olvides de NO VERSIONAR sdkconfig una vez que hayas puesto los valores de conexión de la WiFi. Lo que hago yo es:

idf.py menuconfig -> Save

git add sdkconfig

git commit -m "xxx"

idf.py menuconfig

Example Connection Configuration --->
  [*] connect using WiFi interface
  (xxxxxxx) WiFi SSID
  (xxxxxxx) WiFi Password

Pero si tenés otros valores secretos se empieza a complicar. De un modo u otro, repito, NO VERSIONES SECRETOS.

 

Ajustes del código

 

Tomamos como base esp-idf-lib/examples/dht/main/main.c

includes

#include <stdio.h>
#include "dht.h"

constants

static const dht_sensor_type_t sensor_type = DHT_TYPE_DHT11;
static const gpio_num_t dht_gpio = 17;

La URL

#define WEB_SERVER "example.com" -> la IP que uses
#define WEB_PORT "80" -> el puerto que uses
#define WEB_PATH "/" -> según mi ejemplo "/collect.php"

static char *REQUEST_GET =
        "GET " WEB_PATH "/?t=%d&h=%d HTTP/1.0\r\n"...

La idea es reemplazar esos dos %d con sprintf(), para eso incluí <stdio.h>, con los valores leídos.

Las variables

char send_buf[256];

int16_t temperature = 0;
int16_t humidity = 0;

La lectura del sensor

while(1) {
  if (dht_read_data(sensor_type, dht_gpio, &humidity, &temperature) == ESP_OK) {
    ESP_LOGI(TAG,"Humidity: %d%% Temp: %dC\n", humidity, temperature);
    sprintf(send_buf, REQUEST_GET, temperature, humidity);
    ESP_LOGI(TAG,"sending: \n%s\n",send_buf);
 } else {
    ESP_LOGE(TAG,"Could not read data from sensor\n");
 }

y la escritura, al buffer lo preparamos con el sprintf() anterior.

if (write(s, send_buf, strlen(send_buf)) < 0) {...



Acá el código entero resultante con propuestas de ejercicios intercalados, no deberías necesitarlo, pero no molesta. El proyecto completo en github

 

#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "lwip/dns.h"
#include "dht.h"


/* Constants that aren't configurable in menuconfig */
#define WEB_SERVER "192.168.1.102"
#define WEB_PORT "8080"
#define WEB_PATH "/collect.php"

static const dht_sensor_type_t sensor_type = DHT_TYPE_DHT11;
static const gpio_num_t dht_gpio = 17;


static const char *TAG = "temp_collector";

static char *REQUEST_GET = "GET " WEB_PATH "/?t=%d&h=%d HTTP/1.0\r\n"
    "Host: "WEB_SERVER":"WEB_PORT"\r\n"
    "User-Agent: esp-idf/1.0 esp32\r\n"
    "\r\n";

// Ejercicio: enviar POST    
// Ejercicio: enviar json

static void http_get_task(void *pvParameters)
{
    const struct addrinfo hints = {
        .ai_family = AF_INET,
        .ai_socktype = SOCK_STREAM,
    };
    struct addrinfo *res;
    struct in_addr *addr;
    int s, r;
    char recv_buf[64];

    char send_buf[256];

    int16_t temperature = 0;
    int16_t humidity = 0;
 
    while(1) {
        if (dht_read_data(sensor_type, dht_gpio,
                &humidity, &temperature) == ESP_OK) {
            ESP_LOGI(TAG,
    "Humidity: %d%% Temp: %dC\n", humidity / 10, temperature / 10);
            sprintf(send_buf, REQUEST_GET, temperature / 10, humidity / 10);
        ESP_LOGI(TAG,"sending: \n%s\n",send_buf);
        } else {
            ESP_LOGE(TAG,"Could not read data from sensor\n");
        // Ejercicio: enviar mensaje
        }

        int err = getaddrinfo(WEB_SERVER, WEB_PORT, &hints, &res);

        if(err != 0 || res == NULL) {
            ESP_LOGE(TAG, "DNS lookup failed err=%d res=%p",
                      err, res);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            continue;
        }

        addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
        ESP_LOGI(TAG, "DNS lookup succeeded. IP=%s",
                        inet_ntoa(*addr));

        s = socket(res->ai_family, res->ai_socktype, 0);
        if(s < 0) {
            ESP_LOGE(TAG, "... Failed to allocate socket.");
            freeaddrinfo(res);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... allocated socket");

        if(connect(s, res->ai_addr, res->ai_addrlen) != 0) {
            ESP_LOGE(TAG,
                 "... socket connect failed errno=%d", errno);
            close(s);
            freeaddrinfo(res);
            vTaskDelay(4000 / portTICK_PERIOD_MS);
            continue;
        }

        ESP_LOGI(TAG, "... connected");
        freeaddrinfo(res);

        if (write(s, send_buf, strlen(send_buf)) < 0) {
            ESP_LOGE(TAG, "... socket send failed");
            close(s);
            vTaskDelay(4000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... socket send success");

        struct timeval receiving_timeout;
        receiving_timeout.tv_sec = 5;
        receiving_timeout.tv_usec = 0;
        if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO,
           &receiving_timeout, sizeof(receiving_timeout)) < 0) {
            ESP_LOGE(TAG,
              "... failed to set socket receiving timeout");
            close(s);
            vTaskDelay(4000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... set socket receiving timeout success");

        /* Read HTTP response */
        do {
            bzero(recv_buf, sizeof(recv_buf));
            r = read(s, recv_buf, sizeof(recv_buf)-1);
            for(int i = 0; i < r; i++) {
                putchar(recv_buf[i]);
            }
        } while(r > 0);

        ESP_LOGI(TAG, "... done reading from socket. Last read return=%d errno=%d.", r, errno);
        close(s);

        for(int countdown = 10; countdown >= 0; countdown--) {
            ESP_LOGI(TAG, "%d... ", countdown);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
        ESP_LOGI(TAG, "Starting again!");
    }
}

void app_main(void)
{
    ESP_ERROR_CHECK( nvs_flash_init() );
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    ESP_ERROR_CHECK(example_connect());

    xTaskCreate(&http_get_task, "http_get_task", 4096, NULL, 5, NULL);
}


Si hubieras usado el ejemplo http_request sin tocar y vez esa basura no te preocupes, es que el servidor te ha dado la respuesta comprimida, observá el Content-Encoding: gzip, no tenés error.


example.org
example.org


Errata


Me faltó mencionar que hay que conectar el sensor a algún pin, en este caso usé el 17

static const gpio_num_t dht_gpio = 17;

y no olvides poner la resistencia entre el positivo (3.3v) y el cable de datos.

2022/02/21

Primer contacto real con PIC16

Tras haber asistido a un seminario introductorio a PIC16, debido a que los kits fueron entregados tras éste, me quedaron pendientes dos actividades: el blinky y ver si unas instrucciones muy específicas bcf y bsf, no serían reemplazables por unos ingeniosos ands y ors.

 

El blinky

 

Hasta donde entendí, a diferencia de los AVR, estos PIC siempre necesitan una referencia externa para el clock, hay que ponerle un cristal y unos capacitores... no tan fácil, en el curso no hay ningún diagrama de conexión, como que está asumido que vos ya sabés electrónica digital y lo que no conocés es PIC. Menciona por un lado que sólo se van a usar dos pines para programar (TX y RX) y luego dice "Comparte el control del pin MCLR", ¿mmh, qué querrá decir? Obviamente pedí el diagrama pero para hacer más interesante esta experiencia lo voy a recibir cuando haya tenido éxito o fracaso.

Entonces, al buscar diagramas relacionados a cosas como "pic16f tiny bootloader circuit diagram and usb uart adapter" lo segundo mejor que encontré fué un proyecto donde muestra que se usa RTS para pegarle a MCLR (a.k.a. RESET), pero eso es a la salida de un MAX232, la idea es usar el adaptador USB-UART provisto en el kit (dicho sea de paso, ese kit debería haber traido el cristal, los capacitores...)

 

Usando RTS para el RESET
Usando RTS para el RESET


Finalmente, llegúe a un tutorial que al final dice:

 

Download the complete project folder from the below link:
Hardware design Files and Code Library 

 

Sin RTS
Sin RTS


perfecto, es lo más sencillo posible que parecería funcionar, al reset habría que darle con el dedo.

 

Ahora, a tomar posesión del chip, lo primero es identificar las patitas:

 

Pinout colorizado por función
Pinout colorizado por función

Cada color es un puerto, en grís los mínimos necesarios para programarlo.


A pensar un poco, muy poco, el diagrama del tutorial es lo que hay que hacer.


Circuito armado
Circuito armado

A la derecha Tx y Rx al USB-TTL. Lo alimento con una fuente externa.


Antes de investigar nada más, a ver si funciona. Hay que bajar el tinybootloader para linux y probar...

 

En la versión 0.6 al bajar el .deb falla por dependencia imposible de satisfacer, python-gtk2,buscando, hallamos https://osdn.net/projects/sfnet_tinybldlin/releases/ que dice tener la versión 0.8.1, veamos... nop, tambien necesita python-gtk2

              Depends: python-gtk2 but it is not installable
              Depends: python-cairo but it is not installed
              Depends: python-gobject but it is not installed
              Depends: python-serial but it is not installable

Veamos de instalarlo basándonos en https://techviewleo.com/install-python-with-virtualenv-on-linux-mint/

sudo apt-get update

sudo apt-get upgrade

sudo add-apt-repository universe

sudo apt-get update

con algunos

apt-cache search python 

determinamos que python-cairo y python-gobject estan disponibles, hay que usar pip para el resto

curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py

sudo python2 get-pip.py

ok, pero luego no puedo hallar los paquetes faltantes con pip


Hay varios caminos: 

  • resolver las dependencias
    • es en lo que vengo fallando
  • buscar otro programa
    • no está apareciendo
  • instanciar una virtual con una distro antigua que soporte python2
    • quizás tampoco pueda resolver las dependencias
    • puede ser que mplab no funcione ahí
      • no importa, sólo la usaría para flashear
  • usar wine
    • seguramente se me complique el acceso al puerto serial
    • chicken
  • instanciar una virtual con windows
    • se me puede complicar el acceso al puerto serial
    • chicken
  • actualizar el programa a python3
    • y hacer un aporte a la humanidad...
    • pero puede ser mucho tiempo
  • evitar el bootloader
    • esto significa faltar al espíritu de la experiencia original
    • tendría que armar o comprar el programador
      • no me interesa volver a hacer nada con PIC, quizás programar otro que tengo en un cajón

o mucho mejor aun, buscar un poco más hasta encontrar que 


https://github.com/lcgamboa/tinybldlin

y un

sudo apt install python3-serial


abre el programa, pero... siempre hay un pero, dice que:


Tiny PIC BootLoader
Tiny PIC BootLoader

...no lo encuentra. Mirá fijo la captura y vas a ver que en la terminal desde donde lo abriste, dice que aprietes el reset. Esta captura no es sobre la primera iteración, donde esa terminal había quedado oculta.

Lo aprieto y nada...

Siguiento mi método de armar un mail para no mandarlo, donde fuí documentando todo lo hecho para solicitar ayuda, descubrí que había conectado mal Rx, así que a desoldar... y anda.

Situación: hay que apretar el reset ante cada operación, ok, razonable.

Puede hacer falta por única vez:

  •  Tools -> Options -> Embedded -> Build Tools -> Scan for Build Tools 

Luego, para cada proyecto:

  • New project -> Microchip Embedded -> Standalone project
  • family -> Baseline 8-bit MCUs (PIC10/12/16)
  • device -> PIC16F874A
  • tool -> simulator
  • header -> none
  • compiler -> XC8 (v2.32) o similar
  • new file -> main.c

#include <xc.h>
#include <pic16f877a.h>
#include <stdio.h>

#define _XTAL_FREQ 4000000

void main(void) {
    TRISB = 0;
    PORTB=0;
    while(1) {
        PORTBbits.RB0=0;
        PORTBbits.RB1=0;
        PORTBbits.RB7=0;
        __delay_ms(500);
        PORTBbits.RB0=1;
        PORTBbits.RB1=1;
        PORTBbits.RB7=1;
        __delay_ms(500);
    }
    return;
}

  • Build
  • copiar a tinypicbootloader la ruta tipo:

/home/carlos/Desktop/blinky.X/dist/default/production/blinky.X.production.hex

  • write flash
  • reset
Listo, el blinky funciona.

La parte ingeniosa del análisis bcf -> andwf queda para otro día,


2021/11/17

Notas al seminario de MCU PIC16F

He asistido a un interesante seminario de explicaciones del PIC16F a cargo de Andrés Bruno Saravia que es Certified Trainer de Microchip, autor del libro "Arquitectura y Programación PIC16F1939 En Lenguaje C con XC8", comparto acá mis notas y la adaptación a linux.

 

Ideas


Un concepto que ya tenía aunque no recuerdo de donde ni tenerlo bien conciente es el de la compresión de instrucciones, me llevó un rato darme cuenta que no era una compresión real, sino a la manera de referirse a la relación entre el número de instrucciones en C y las necesarias en assembly.


Otro concepto que había sufrido antes sin darme cuenta es el de arquitectura cerrada vs abierta. Los 8051 tiene arquitectura abierta, esto es que se pueden tomar unos pines y exponer el bus de datos, direcciones y control para agregarle una memoria, por ejemplo. Los microcontroladores como los PIC y AVR que he usado son cerrados, no hay buses paralelos afuera.

Me llamó la atención que siendo uno de los objetivos que la ISA fuera bien chiquita, que existieran:


bcf f,b borra bit f
bsf f,b setea bit f

que según entiendo bien se podrían implementar con

 

andwf f, d AND W con f
iorwf f,d OR inclusiva W con f 

 

Cuando me junte con el kit, probaré. ¿Qué? ¿Que podría simular? Ni a palos, tengo otras cosas que hacer estos días, pero es una buena idea, lo probaré tanto con el chip como como excusa para aprender a simular.

 

El kit


Entiendo que el kit que recibiré no incluye los capacitores ni el cristal, evidentemente este seminario está más orientado a profesionales que a hobbystas como yo, que se asustan ante la más mínima insinuación de electrónica digital. No importa, son unos pesitos más, no pasa nada.

 

La adaptación a linux

 

Toolchain


Se trata de MPLAB-X, no hay nada que adaptar, sólo hay que bajarla del sitio oficial. Me pone un poco incómodo que tanto para la instalación de ésta como para la instalación de los compiladores haga falta hacerlo como root. Esto garantiza que siempre lo usaré en una virtual.

 

El tiny bootloader

 

¿Cómo hacés para cargarle un programa a una computadora? Lo bajás de internet, lo pasás por pendrive, diskette, lo que sea. Estos bichitos no tienen tantas posibilidades, en el caso de PIC se llama ICSP, que no es una certificación sino In-Circuit Serial Programming, parecido supongo a ISP (In System Programming) de los AVRs,  sólo le ocupan dos o tres pines y necesitan un programador, que son $$$ más y como yo sólo estoy de visita no lo pienso adquirir, con mis ATMega328p aún me sobra para lo que necesito.

Como hay otra gente a la que tampoco le cierra gastar esa plata ni perder N pines, existe un método que es cargar un programa que cuando arranca se fija si en el puerto serie hay unos ciertos códigos y si los encuentra considera que son el programa y lo carga en la flash. Si pasa un rato y no hay nada en el puerto serie, ejecuta lo que tenga de antes, supongo que nada si no hay nada.

 

En el seminario se utilizó tinybld198 que gentilmente fué provisto por Andrés y corre en windows. A buscar y lo primero que aparece es tinybldlin que ni siquiera hay que compilar pues funciona con python.

 

El adaptador


Ni hace falta que lo diga pero lo haré, no necesita agregar ningún driver, no entiendo por qué windows tiene esa tara, pasa lo mismo con edu-ciaa-fpga, usbasp, lo que sea.

Sí puede hacer falta toquetear udev y permisos. Los pasos ya los he registrado en otras entradas pero no me ofende volver a hacerlo...

 

Conclusión


A mi me encantó, no sé si tanto si ya hubiese sabido antes de PIC, tuvo quizás demasiado tiempo de no demo, pero el tiempo de demo fue bien respetado, al punto que el seminario duró como cuarenta minutos de más.

 

Cuando me junte con el kit, sigo... en https://seguridad-agile.blogspot.com/2022/02/primer-contacto-real-con-pic16.html




2021/11/03

Ejemplo de SID

Aunque a todos nos debe haber ocurrido, es la primera vez que me ocurre a mi de modo tan evidente, comparto la experiencia.

Me contactaron unas personas para ver unos temas de seguridad y uno de los items era "revisar las seguridad de un firmware".

Los actores somos el end user, mi interlocutor que es el cliente y yo.

Este firmware era el resultado de la siguiente situación, el que hayan dos dispositivos y cada uno con dos firmwares representa las posibilidades, el end user termina conectándose a un firmware en un dispositivo:

 

Situación actual
Situación actual

Los end users tienen un dispositivo de hardware que para mejor interactuar con el  sistema del cliente requiere cambiar un aspecto de su configuración con frecuencia. Como la alta frecuencia de ese cambio no está prevista por el fabricante del firmware, no ofrece almacenar configuraciones alternativas, mediante la interfaz web del firmware hay que pisar con la nueva la existente en lugar de dar de alta varias y sólo elegir luego la activa.

La idea del cliente era tomar el firmware que es opensource y extenderlo.


Actualización de Firmware
Actualización de Firmware

Inmediatamente me generó incomodidad debido a que el firmware es una pieza delicada y cualquier cambio o extensión aumenta la superficie de ataque y aumenta la responsabilidad, tanto de seguridad como de funcionamiento. Además, pensaba estos inconvenientes adicionales:

  • Si hay upgrades al firmware oficial, el cliente va a tener que portar los cambios.
  • Si el cambio es aceptado e incorporado al firmware oficial, probablemente tenga que mantenerlo.
  • Hasta acá quizás es aceptable, pero si hubieran distintos firmwares, se multiplica el esfuerzo.

Esto viene de la mano de un concepto de seguridad que dice que a menos que te dediques a la criptografía, no diseñes criptografía, sólo usala, pues vas a meter la pata. En este caso aplica si no te dedicás a hacer firmware, no diseñes firmware.

 

La primera propuesta que se me ocurrió fue implementar una web que haga de fachada ante esos dispositivos y mediante un poquito de scrapping, opere contra el dispositivo, almacenando las configuraciones alternativas, que de paso no haría falta que las cargue el end user, sólo seleccionarlas.

 

Con servidor web en el medio
Con servidor web en el medio
 

El scrapper, al que elegantemente llamé "conector", sería uno por cada versión de firmware de cada dispositivo. Este concepto de conector se repite en todas mis propuestas.


Nuevamente una mala sensación, las credenciales de los dispositivos de los end users pasan por la infraestructura del cliente, eso aumenta su responsabilidad en términos de seguridad. Aunque sólo reciba las credenciales y las use sin persistirlas, que es una buena reducción de superficie, resulta inútil frente un APT en la infraestrucura del cliente.

Y están estos problemas adicionales:

  • El dispositivo debe ser accesible desde la red del cliente
  • Hay que darle una nueva credencial ante este sistema
  • Cuando el end user ingrese la IP del dispositivo sobre el cual operar... podría ser cualquier IP, se podría usar mal por error o a propósito.


Finalmente, evolucioné la idea a elaborar un plugin para el browser, que usando los conectores de scrapping, tomen las configuraciones o de un storage local o de la infraestructura del cliente.

 

Con plugin de browser
Con plugin de browser

Sólo hay que mantener dos versiones, no hace falta actualizar permanentemente, pues el cambio de arquitectura de los plugins de los browsers no es para nada frecuente. Las credenciales del dispositivo quedan del lado del end user

Los ataques que quedan pasan por influenciar al end user para que ponga la IP de un dispositivo atacante en lugar del suyo y así tomarle las credenciales, pero quien caiga en ese ataque probablemente caiga tambien en "dame las credenciales".

Una posibilidad extra es hacer una aplicación mobile, pero quizás nuevamente estás entrando en terreno desconocido y aumentando la superficie de ataque.


Por si no te diste cuenta, SID es un término nuevo que inventé, o quizás reinventé, pero no tengo ganas de andar buscando a ver si ya existe, Security Influenced Design, en la línea de TDD (Test Driven Design de Kent Beck), BDD (Behavior Driven Design), DDD (Domain Driven Design de Evans). No llega a guiar el diseño pero si lo influencia.

2020/05/01

xzoom mejorado

Me había quedado muy incómodo con el hack de arrastrar la carpeta para que se viera en la ventana magnificada el cursor, así que me puse a investigar. A diferencia de lo que suelo hacer que es ir mostrando todos los caminos equivocados que tomé y lo que aprendí, voy a mostrar sólo el camino feliz.

Entre la m y la p, hay una región en negativo


Tras algunos intentos fallidos aprendí a bajar el código fuente, tan fácil como:

$> mkdir sources
$> cd sources
$> apt source xzoom


Conviene crear la carpeta contenedora por que te tira todo donde estés. Para crear el Makefile:

$> xmkmf

Si no existe lo arreglás con el sudo apt get que te sugiere. Luego compilás con:

$> make


Yo quería apoyarme en lo existente y hacer un fork para luego proponer un pull request, me puse a buscar y hallé:


Le mandé mail al creador original, parece ser un proyecto que ha muerto en su origen, si me constesta, actualizaré.



Me quedé entonces en investigar y se me ocurrió mirar la versión de mbarakatt, ¡bingo! tiene casi todo el código necesario, sólo tuve que acomodarlo y ver como dibujar el cursor.

Para que le sirva a mbarakatt incorporé primero la funcionalidad de prender y apagar el follow mouse y luego seguí construyendo encima.

Lo que voy a hacer para que le sirva a mbarakatt es primero incorporar la funcionalidad de prender y apagar follow mouse, así me queda el código incorporado, luego agregaré lo que quería.


Proceso definitivo




$> mkdir xzoom
$> cd xzoom/
$> apt source xzoom
$> rm *
$> mv xzoom-0.3/* .
$> rmdir xzoom-0.3/
$> git init .
$> git add .
$> git commit -m "Initial commit ..."

Hice un branch follow_mouse y en master dejé lo del cursor.

Finalmente, para actualizar el binario:

$> which xzoom 
/usr/bin/xzoom
$> sudo chown root.root xzoom
$> sudo cp xzoom /usr/bin/xzoom


El código está en https://github.com/cpantel/xzoom.

El código aportado


Para procesar las nuevas opciones de linea de comando:


if(!strcmp(argv[0], "-follow")) {
  follow_mouse = True;
  continue;

}

if(!strcmp(argv[0], "-no-follow")) {
  follow_mouse = False;
  continue;
}

if(!strcmp(argv[0], "-cursor")) {
  show_cursor = True;
  continue;
}
 

if(!strcmp(argv[0], "-no-cursor")) {
  show_cursor = False;
  continue;
}



El cambio de las opciones en tiempo de ejecución:


case 'c':
  show_cursor = ! show_cursor;
  break;
 

case 'f':
  follow_mouse = ! follow_mouse;
  break;




Le agregué los condicionales a seguir el mouse:

if (follow_mouse || show_cursor ) {
  for (i = 0; i < number_of_screens; i++) {
    result = XQueryPointer(display, root_windows[i], 

               &window_returned, &window_returned,
               &root_x, &root_y, &win_x, &win_y,
               &mask_return

             );
    if (result == True) {
      break;
    }
  }
  if (result != True) {
    fprintf(stderr, "No mouse found.\n");
    return -1;
  }
  if (follow_mouse) {
    xgrab = root_x - width[SRC]/2;
    ygrab = root_y - height[SRC]/2;
  }

}


Y el corazón, que fué medio tiro al arco zen, por puro instinto, inspirándome en una pregunta que encontré por ahí:


if (show_cursor) {
  long pixel = 0;
  int cursor2x = ( root_x - xgrab ) * magx;
  int cursor2y = ( root_y - ygrab ) * magy;
 

  if (cursor2x < CURSOR_RADIUS)
    cursor2x = CURSOR_RADIUS;
  if (cursor2y < CURSOR_RADIUS)

    cursor2y = CURSOR_RADIUS;

  if (cursor2x > ximage[DST]->width)

    cursor2x = ximage[DST]->width - CURSOR_RADIUS;
  if (cursor2y > ximage[DST]->width)
    cursor2y = ximage[DST]->height - CURSOR_RADIUS;

  for (int x = cursor2x - CURSOR_RADIUS;

       x < cursor2x + CURSOR_RADIUS 
         && x < ximage[DST]->width;
       x++
      ) {
    for (int y = cursor2y - CURSOR_RADIUS;

         y < cursor2y + CURSOR_RADIUS
           && y < ximage[DST]->height;
         y++
        ) {
      // Invert the color of each pixel
      pixel = XGetPixel(ximage[DST], x, y);
      XPutPixel(ximage[DST], x, y, ~pixel);
    }
  }
}



Los limitantes entre 0 y width y height estan para que el cursor se detenga en el borde y te dé un indicio de dónde está. El de 0 tambien está por que si no crashea.

El código es horrible, son mil lineas, todo en main, variables globales. Si fuera a completar lo faltante que es que funcione correctamente mostrar cursor cuando hay rotación, lo refactorizaría todo, pero difícil que ocurra.

2020/04/30

FSM vs SQL, algunas consideraciones de correlación de eventos


Hace como un año comencé a desplegar unas alarmas en el trabajo utilizando FSM para procesar los logs y obtener alertas tempranas ante ciertos incidentes.

Luego me dí cuenta que pude haberlo hecho utilizando consultas SQL, lo cual me llevó a reflexionar mucho sobre el tema, hacer algunas prueba y preparar una charla que dí en el trabajo de modo virtual pero no podré dar afuera por que todas las conferencias medio que están caidas.

Esto viene a ser un resumen de lo que pudiste haber visto en FLiSOL, H4CK3D, OWASP Latam Tour, LACNIC y no digo Ekoparty por que nunca me aceptan nada.

Voy a asumir que tenés una idea de los temas, que sólo te estoy aportando las relaciones entre las ideas. Además yo no soy ninguna autoridad de conocimiento, sólo estoy expresando opiniones más o menos bien sustentadas.



Voy a usar el siguiente vocabulario, los dos primeros corresponden al uso común y el último lo uso para manejarnos ahora.

Evento: cada cambio sin connotación positiva o negativa
Incidente: un cambio con connotación negativa
Caso: conjunto de eventos en análisis que conducen a un incidente.

Un caso simple


Partamos de un sencillo requerimiento atemporal, no hay tiempo ni información variable involucradas.


Si hay una conexión a la IP_perimetral en el puerto 8081 desde una IP_externa que no está en la red 211.10.10.0/24 y tiene cierto "DATO", quiero una alarma, evitarlo o incluso agredir.

Esta es un típica regla stateless, se puede resolver con lógica combinacional:

[grafico logico]

IP destino en IP_perimetral
   AND
puerto es 8081
   AND
IP origen NOT en red 211.10.10.0/24
   AND
mensaje contiene "DATO"

Tambien con una consulta SQL:

SELECT * FROM conexiones 
   WHERE
      ip_destino IN (SELECT  ip FROM red_perimetral)
   AND
     port_destino = 8081
   AND
     ip_origen NOT IN ( SELECT ip FROM red_externa)
   AND
     datos like “%DATO%”;


O con una FSM de un solo estado, que contendría la lógica combinacional previamente citada, en pseudo-C:

if ( 
  pertence( IP_destino , IP_perimetral) )
  & puerto == 8081
  & ! pertenece(IP_origen, 211.10.10.0.24)
  & strpos("DATO", mensaje) {
    generar_alerta();
}



Algunos atributos apropiados para nuestros ejemplos:
  • Origen
  • Destino
  • Tipo de mensaje
  • Contenido del mensaje
  • Longitud del mensaje
  • Horario

Un caso complejo



Si hay un scan desde una IP_externa y desde esa misma IP hay luego una conexión con un DATO y luego se ve ese DATO en una copia entre nodos internos de una IP_interna_origen a una IP_interna_destino y finalmente hay una conexión saliente de esa ip interna de destino con ese DATO, quiero una alarma, evitarlo o incluso agredir.

Que es difícil de leer, mejor no lo leas, leé esto:

Si se da la siguiente secuencia de eventos
  • Scan desde una IP_externa
  • Conexión desde esa IP_externa con un DATO
  • Copia de datos entre dos nodos con ese DATO desde una IP_interna_origen a una IP_interna_destino
  • Conexión saliente de la IP_interna_destino con ese DATO
 entonces quiero una alarma, evitarlo o incluso agredir

Fijate que las partes coloreadas no están predefinidas, son variables que deben
concidir entre las distintas partes de la reglas

A los atributos previamente mencionados se suma "Relación entre los mensajes".

Lógica combinacional


Este es el terreno de Boole, es prácticamente instántanea, corre a la velocidad máxima de propagación entre compuertas si está implementada en hardware, que es factible usando FPGA. Si corre en software, son pocas y simples instrucciones. Cuando se le agrega memoria, se llama secuencial, siendo un caso particular es la FSM.

FSM


Dice wikipedia que:

Un autómata finito (AF) o máquina de estado finito es un modelo computacional que realiza cómputos en forma automática sobre una entrada para producir una salida.

Que es lo mismo que hace cualquier proceso o programa, lo específico es que es un cierto modelo computacional, mirá los componentes de una FSM:


  • estados
  • entradas
  • salidas
  • función estado(estado,entrada)
  • función salida(estado,entrada)
  • estado inicial

Un proceso genérico no tiene necesariamente la función de estado, ni siquiera estados.

Las funciones de entrada y salida son combinacionales, quizás con efectos colaterales cuando hay más memoria que estado en sí.


Un ejemplo muy natural por decir de algún modo es un operating system scheduler que es su expresión más baja se reduce a esto:




Lo que está diciendo es que cuando se crea un nuevo proceso, va al estado "Ready To Run". Cuando el OSS decide que corra, Running, del cual puede salir cuando termina, cuando se le termina el tiempo asignado de ejecución y el OSS lo regresa a R2R o cuando pide una operación que implica largos tiempos de espera como acceso a un periférico. El sistema operativo detecta eso y elije mandarlo a dormir para liberar la CPU para otro proceso. Cuando la operación de entrada salida concluye, el proceso pasa nuevamente a R2R y así.


Como se implementa, puede ser como en mi demo con el estado atributo en la instancia, pero tambien puede estar el objeto (o estructura) en una tabla y una de sus columnas ser el estado. Tambien y me gusta más intuitivamente pues nunca implementé un OSS, en tablas o listas separadas, una para cada estado.

Consulta SQL


Asumo que sabés al menos que es un join sencillo.

En el contexto de lo que estamos analizando, estas son las principales características para comparar entre FSM y consulta SQL:

En la FSM el tiempo está implícito, hace falta que los eventos lleguen ordenados, falla si no. No hace falta guardar estos eventos, sí algún que otro dato para parametrizar las reglas y para el reporte final. Esos datos y el overhead de las instancias de cada caso genera un consumo creciente de memoria. Dado que da una respuesta de muy baja latencia, puede ser usada en situación de bloqueo, esto es que no sólo alerte sino impida la acción final del ataque. Es más difícil de programar pues es... programar, justo.

Las Consultas SQL por su lado necesitan el tiempo pero este está explícito en el timestamp del mensaje y necesita conservar los mensajes, de lo cual se encarga el DB Engine con todo sus ventajas de escalamiento. No lo veo mucho en situación de bloqueo, quizás si para un sistema transaccional pero no para algo donde la latencia sea crítica. Es más fácil de programar pues no es programar, es escribir SQL, lo cuál al comienzo es más difícil cuando es complejo, pero luego es más de lo mismo.


Una FSM puede alimentarse de un batch ordenado. O sea que tambien se puede usar para detectar en archivos históricos. Me imagino que no debe ser más eficiente que DB pues las DB estan diseñadas para ser eficientes en ese escenario.

Como machete, FSM vs Query SQL:



FSM
Query SQL
Tiempo
Implícito
Explícito
Orden de eventos
Indispensable
Indiferente
IO
Stream
Stream/batch
Almacenamiento
Descartable
Indispensable
Memoria
Creciente si no se purga
DB Engine (*)
Real Time
Hard (sniffer)
Soft
Modo
Detección y Bloqueo
Detección
Escalamiento
Provisto por S.O.
Provisto por DB
Conocimientos
Programación
SQL
Cambios lógica
Difícil
Fácil
Embebible
Si
Mmmh


Con embebible me refiero a que se podría tomar un microntrolador y/o FPGA y hacer un modulito que se conecte a la red y cumpla alguna función digna de mención, no tanto con una DB, para la cual hace falta un sistema operativo detras, mmmh, por eso, mmmh, no.

Características compartidas

  • Hay que tener en cuenta un "tiempo de vida", un "timeout", tras el cual un caso debe ser desestimado en el caso de FSM o una antigüedad de log ignorada en el de SQL. Esto reduce el tamaño de la DB/espacio en memoria.
  • Pueden haber alertas intermedias, en el caso de FSM simplemente emitir mensajes en estados previos al final, en el de SQL consultas que no incluyan las últimas condiciones, una especie de copiar->pegar->podar de la consulta.

Para SQL hay un optimización híbrida, en lugar de ejecutar siempre toda la consulta, buscar la condición del último evento, en nuestro ejemplo una conexión hacia el exterior, si existe, hacer la consulta completa. Quizás las optimizaciones que hacen los DB Engines hacen eso o se les puede sugerir, sé muy poco de DB.

Dicho de otra manera, cuando llega el último evento, me fijo si han ocurrido las condiciones para llegar hasta ahí.

Hice una demo que está en github, te cuento:

Modificá export.php y generás la base de datos para las Consultas SQL, que están en el archivo... consultas.sql.

SimpleFSM.java es una POC para casos no solapados, es para concentrarse en un ejemplo de FSM y en la lógica y cuestiones secundarias como leer de STDIN.

FSM.java es la implementación más correcta, donde una una colección de casos, de paso, las clases deberían llamarse CasoSimple y Caso, no el nombre de la implementación, pero cuando lo hice estaba mas concentrado en la FSM que en las recomendaciones de Domain Driven Design.

Fijate que al pasar de SimpleFSM a FSM el evento que lleva al primer estado es extraido de la  lógica común. Para entender bien mirá la evolución por el paso intermedio, FSM2, que debería llamarse FSM_fail, donde estoy haciendo dos cosas a la vez: la FSM y la gestión de la colección.

Lo correcto es hacer FSM y de ahí fijarse como optimizar para que los loops terminen antes si pueden y esas cositas.