viernes, 15 de marzo de 2013

Seguimos con los Coches.

Aparentemente esta entrada es casi identica a la anterior, pero por dentro tiene mucha mas miga.

Existe un software para simuladores (aviones o coches) disponible en www.x-sim.de y mientras lo investigaba para un futuro simulador con movimiento, descubri que tambien permite realizar paneles de instrumentos, sobre todo orientado a los simuladores de coches, asi que decidi actualizar el display.

Este software tiene varias ventajas, por un lado puede utilizarse con varios simuladores, simplemente configurando el perfil adecuado, por otro, tiene un modulo de comunicacion serie, por lo que eliminamos varias capas intermedias, recordemos que antes teniamos:
simulador->servidor iocp->SIOC->script en python->arduino 
y ahora se reduce a:
simulador->software x-sim->arduino

Desde aqui podeis bajaros el programa para arduino:
https://www.dropbox.com/s/nwc9pek3ynyj2qw/xsim_rpm5.ino

Esta ampliamente inspirado en el que aparece en este post: http://www.x-sim.de/forum/viewtopic.php?f=40&t=155

Tiene dos mejoras, por un lado la gestion de las RPM maximas y minimas, y por otro lado tiene una conexion serie mas fiable, que soporta sin problemas interrupciones y desconexiones del puerto serie.

Funcionamiento:

En el display numerico aparecen:
Indicador de Bandera (no funciona, deberia aparecer cuando hay bandera amarilla, pero el xsim no lo exporta).
La marcha actual.
La velocidad en km/h.

Los led se encienden progresivamente y se ponen todos rojos con brillo al alcanzar las maximas rpm, el valor de minimas rpm (cuando se enciende el primer led) y maximas (cuando se ponen en rojo) es configurable con los botones, este es su funcionamiento:
  • Boton 1 - Establece el modo F1, rpm minimas = 5000, rpm maximas = 18500
  • Boton 2 - Establece el modo GT, rpm minimas = 0, rpm maximas = 9000
  • Boton 3 - Recupera de la eeprom los valores de rpm personalizados (modo CU).
  • Boton 4 - Guarda en la eeprom los valores de rpm personalizados, al arrancar el arduino lee esto valores y los utiliza, asi para que siempre arranque en modo F1 pulsamos el boton 1, luego el 4 y confirmamos que queremos guardarlos pulsando otra vez el 1.
  • Boton 5 - decrementa el valor de rpm minimas
  • Boton 6 - incrementa el valor de rpm minimas
  • Boton 7 - decrementa el valor de rpm maximas
  • Boton 8 - incrementa el valor de rpm maximas

 Ademas de la parte visible del programa, las salidas 5 y 6 estan configuradas como servos de modelismo, de cara a experimentar con la simulacion de movimiento de 2DOF.

Configuracion de x-sim.de

El archivo de configuracion se puede bajar de aqui:
https://www.dropbox.com/s/ldc191rbhn2glzn/display.rn2

Basicamente el truco esta en configurar adecuadamente la parte matematica, asi como para el valor de velocidad y rpm utilizamos 16bits y un valor siempre positivo, hay que poner el valor maximo de esa señal en 65535, y ajustar el offset a la izquierda del todo.
Para las marchas, utilizamos 8bits, y el valor puede ser negativo, asi que ajustamos a un valor maximo de 128, simetrico, y el offset lo dejamos a 0.

La cadena que envia, usando el modulo SIO, cada 20ms, (puede ser menos), seria:
R~01~~04~S~02~G~03~~05~~06~~07~
Siendo 01 las RPM, 02 la velocidad, 03 la marcha, 04 la bandera, 05 reservado para un futuro force feedback del pedal de freno (8bits), y 06 y 07 (8bits) las posiciones de los 2 servos para la simulacion de movimiento de la cabina.

Como veis, tiene alguna cosa añadida que no esta un implementada, pero para tener un display delante del volante esta perfecto

EDITADO: Nueva version del software, los led funcionan diferente segun sea F1 o GT. Aqui: https://www.dropbox.com/s/uwkqgulp2bohj7a/xsim_rpm6.ino



viernes, 27 de abril de 2012

Need for Speed

Bueno, despues de un tiempo vuelvo a la carga, por un lado con un modulo TM1638 que me quema en las manos, y por otro con la pregunta que me hicieron de si era posible aumentar la velocidad de comunicacion entre el arduino y el script en python.
Empezemos por lo segundo, si, se puede, solo hay que en arduino modificar la primera linea de la funcion setupserial() y cambiar el 9600 por 115200, por ejemplo, y despues en el script en python cambiar la linea:
se = serial.Serial(11)
por
se = serial.Serial(11,115200)
(11 es por que a mi el arduino me aparece como COM12 y es una menos)


El TM1638 es un modulo que venden entre otros sitios en dealextreme, lo hay con los numeros en verde y en rojo, ademas de eso tiene 8 led bicolores (rojo+verde y en teoria tambien naranja, aunque a mi el naranja no se me ve muy naranja) control de brillo y 8 pulsadores, y se pueden conectar en cadena hasta 6, funciona a 5 voltios, utiliza 2 lineas de I/O + una por modulo, y existe una libreria para controlarlo con arduino.

Y por otro lado esta rfactor, un simulador de carreras para windows que... bueno.... es perfecto, o casi.

En opencockpits hay un plugins, que permite que rfactor sea un cliente IOCP, asi que basta con ejecutar SIOC, y ya tenemos el enlace rfactor->arduino.

Y algunas lineas de codigo mas tarde tenemos algunas vueltas al circuito de almeria, por si un dia de estos me doy unas vueltas en un porsche. jeje

La parte tecnica:

Con esto he llegado al limite de comunicaciones con el arduino, he tenido que reducir muchisimo el trasvase de datos dividiendo por  10000 las rpm.
No se si esto es culpa del arduino o es culpa del plugin de rfactor, pero es algo a tener en cuenta, si necesitamos datos a mucha velocidad.
En el codigo hay algunas cosas que sobra de pruebas, pero al menos funciona:

El script de python es el de siempre (salvo por la velocidad del puerto), no lo voy a poner.

Estas son las lineas que descomente en el archivo rf_IOPC.ini de configuracion del plugin:
var=0101,RPM ; Rev x Min x100
var=0102,SPEED ; Speed  x100
var=0105,GEAR ; Gear -1=reverse, 0=neutral, 1+=forward gears
var=0110,LAP_START; time this lap was started x1000

Este es el programa que ejecuto en SIOC:
Var 0001, name rpm, Value 0
Var 0002, name speed, Value 0
Var 0003, name gearn, Value 0
Var 0004, name lapt, Value 0

Var 0101, name rf_rpm
{
  &rpm = &rf_rpm / 10000
}

Var 0102, name rf_speed
{
  &speed = &rf_speed / 100
}
Var 0110, name rf_lap_time
{
  &lapt = &rf_lap_time / 100
}

Var 0105, name rf_gear
{
  IF &rf_gear = -1      
  {
    &gearn = 1
  }
  ELSE
  {
    &gearn = 0
  }
}

Y por ultimo este es el programa que ejecuto en el arduino:


#include <TM1638.h>
// Variables IOCP a utilizar:
#define RF_rpm 1
#define RF_speed 2
#define RF_gearn 3
#define RF_lap_start 4
#define RF_gear 105
// variables globales necesarias para la decodificacion.
int p=0; // el paso en la decodificacion.
int dat,valor;

TM1638 module(8, 9, 10);
int rpm,spd,gea,gean,lapt;
int mode=0;

#define v0 1

#define v1 13
#define v2 26
#define v3 39
#define v4 52

#define v5 60
#define v6 68
#define v7 76

#define v8 84

#define rpm_step 10.5
void updaterpm(){
  if (rpm >= v8) {
    module.setupDisplay(true,7);
  } else {
    module.setupDisplay(true,2);
  }
  // Los verdes
  if (rpm < v0) {
    module.setLEDs(0x0000); 
  }
  else if ( rpm < v1 ) {
    module.setLEDs(0x0100); 
  }
  else if ( rpm < v2 ) {
    module.setLEDs(0x0300); 
  }
  else if ( rpm < v3 ) {
    module.setLEDs(0x0700); 
  }
  else if ( rpm < v4 ) {
    module.setLEDs(0x0F00); 
  }
  else if ( rpm < v5 ) {
    module.setLEDs(0x1F00); 
    // Los naranjas
  }
  else if ( rpm < v6 ) {
    module.setLEDs(0x3F20); 
  }
  else if ( rpm < v7 ) {
    module.setLEDs(0x7F60); 
  }
  else if ( rpm < v8 ) {
    module.setLEDs(0xFFE0); 
    // Todos rojos
  }
  else {
    module.setLEDs(0x00FF); 
  }
}
void updatespeed(){
  module.setDisplayToDecNumber(spd,0);
}
void updategear(){
  if (gean == 1 ) {
    module.setDisplayToString("r",0,0);
  }
  else if (gea == 0) {
    module.setDisplayToString("n",0,0);
  }
  else{
    module.setDisplayDigit(gea,0,0);
  }
}
void updatetime(){
  module.setDisplayToDecNumber(lapt,0);
}


void setupserial(void){
  Serial.begin(115200);
  Serial.print("Arn.Inicio:");
  Serial.print(RF_rpm);
  Serial.print(":");
  Serial.print(RF_speed);
  Serial.print(":");
  Serial.print(RF_gear);
  Serial.print(":");
  Serial.print(RF_gearn);
  Serial.print(":");
  Serial.print(RF_lap_start);
  Serial.println(":");
}


int isnum(int n){
  if ((n>47) && (n<58)) return 1;
  else return 0;
}

void enviapares(int dat, int valor){
  Serial.print("Arn.Resp:");
  Serial.print(dat);
  Serial.print("=");
  Serial.print(valor);
  Serial.println(":");
}

// Proceso de las comunicaciones serie
void procesapares(int dat, int valor){
  if (dat == RF_rpm){
    rpm=valor;
    if (mode == 0){
      updaterpm();
    }
  }
  if (dat == RF_speed){
    spd=valor;
    if (mode == 0){
      updatespeed();
    }
  }
  if (dat == RF_gear){
    gea=valor;
    if (mode == 0){
      updategear();
    }
  }
  if (dat == RF_gearn){
    gean=valor;
    if (mode == 0){
      updategear();
    }
  }
  if (dat == RF_lap_start){
    lapt=valor;
    if (mode == 1){
      updatetime();
    }
  }
}

void serialstuff(void) {
  int c=0,pr;
  pr=0;
  if (Serial.available()==0) return;
  c=Serial.read();
  do {
    switch (p){
    case 0:
      if (c=='A'){
        p=1;
      }
      pr=1;
      break;
    case 1:
      if (c=='r') {
        p=2;
        pr=1;
      }
      else {
        p=0;
      }
      break;
    case 2:
      if (c=='n') {
        p=3;
        pr=1;
      }
      else {
        p=0;
      }
      break;
    case 3:
      if (c=='.') {
        p=4;
        pr=1;
      }
      else {
        p=0;
      }
      break;
    case 4:  // VALOR ESPECIAL : primera Bifurcacion de respuesta
      switch (c) {
      case 'R':
        p=5;
        pr=1;
        break;
      default:
        p=0;
        break;
      }
      break;
    case 5:
      if (c=='e') {
        p=6;
        pr=1;
      }
      else {
        p=0;
      }
      break;
    case 6:
      if (c=='s') {
        p=7;
        pr=1;
      }
      else {
        p=0;
      }
      break;
    case 7:
      if (c=='p') {
        p=8;
        pr=1;
      }
      else {
        p=0;
      }
      break;
    case 8: 
      if (c==':'){
        p=9;
        pr=1;
        dat=0;
      }
      else {
        p=0;
      }
      break;
    case 9: 
      switch (c){
      case 13: 
        p=20;
        pr=1;
        break; 
      case '=':
        if (dat==0) {
          p=0;
        }
        else {
          p=10;
          pr=1;
        }
        break;
      default:
        if (isnum(c)) {
          dat = 10*dat + (c-48);
          pr=1;
        }
        else {
          p=0;
        }
        break;
      }
      break;
    case 10: 
      if (isnum(c)){
        valor = (c-48);
        p=11;
        pr=1;
      }
      else {
        p=0;
      }
      break;
    case 11: 
      switch (c) {
      case ':':
        procesapares(dat,valor);
        p=8;
        break;
      default:
        if (isnum(c)){
          valor = 10*valor + (c-48);
          pr=1;
        }
        else {
          p=0;
        }
        break;
      }
      break;
    case 20: 
      if (c==10) pr=1;
      p=0;
      break;
    default:
      p=0;
      pr=1;
      break;
    }
  }
  while (pr==0);
}


void setup(){
  setupserial();
  module.setupDisplay(true,7);
  module.setDisplayToString("Init");
  delay(1000);
  module.setupDisplay(true,2);
  module.setDisplayToString("done");
  delay(1000);
  module.clearDisplay();
}


void loop(){  // aqui solo miramos si cambio una tecla y actualizamos el lcd.
  if ((mode != 0) && (module.getButtons() == 0b00000001)) {
    module.setDisplayToString("        ");
    updategear();
    updatespeed();
    mode = 0 ;
  }
  if ((mode !=1) && (module.getButtons() == 0b10000000)) {
    module.setDisplayToString("        ");
    updatetime();
    mode = 1 ; 
  }
  // La parte de comunicaciones
  serialstuff();
}







martes, 6 de marzo de 2012

Get Serious.

Aprobechando que estoy casi acabando la maquina de CNC, y que ya me llego el X-Plane 10, he decidido dejar de hacer pruebas de concepto y empezar a ponerme serio con esto de la cabina.

Asi que manos a la masa y esto es lo que tengo ahora encima de la mesa:

Esta basado en un Cessna 172 "moderno", tendra alguna pequeña diferencia, p.e. el indicador de revoluciones esta en el lugar del ADF, pero es que si no chocaba con el Yoke de Saitek, esta por ver si alargo el eje del mismo que es algo corto, o lo dejo asi, depende de si golpeo mucho con los dedos en el panel al bajar :-)

La idea es muy sencilla, un marco de madera con la forma del panel:

Y varias piezas de madera pintadas y lijadas, mas o menos con la forma del original:

Lo siguiente que voy a hacer es encargar las piezas que me faltan y los marcos para sustituir las pegatinas de la columna de radio por unos que funcionen :-)
Luego el relog, que por fin consegui una mecanica pequeña.
Y despues con la maquina de CNC a crear como loco instrumentos de aguja.

domingo, 26 de febrero de 2012

Indicador de DME: Otro instrumento con Arduino/python/iocp

Tenia por casa un circuito impreso con un panel LCD controlado por un chip Holtek HT1621B, y unos botones.

Me puse a investigar y aqui: http://code.google.com/p/wirejungle/source/browse/trunk/?r=17#trunk%2Flibraries%2FHT1621 que pertenece a http://wirejungle.wordpress.com/about/ encontre cierto codigo que me permitira controlar dicho chip.

Se que en la foto no parece mucho, pero con un poco de imaginacion y la caratula adecuada....

Asi que tras varios cambios para adaptar su codigo a mi placa, un buen rato investigando como estaba cableado el chip al LCD, y aprobechando el sitema explicado en anteriores post de comunicar arduino con X-Plane, me cree un pequeño pero funcional indicador de DME del NAV1, 2 y ADF1.

No es que tenga mucha utilidad para el resto de la gente, pero si tiene algun LCD con el HT1621B, o tienes la pantalla de un analizador de redes SACI como el que yo desguaze,
he dejado en dropbox un zip con el codigo de arduino y demas.

miércoles, 25 de enero de 2012

OpenRadioSim & Arduino: Transpondedor para X-Plane

El fruto y objetivo de mi anterior post, comunicar Arduino con SIOC, es entre otras cosas para hacerme un panel de radios, empezando por el Transpondedor, que resulte economico.

Si la jugada me sale bien lo llamare OpenRadioSim, y sera GPL

Pronto pondre los diseños de la PCB y el programa de arduino completo, asi como el diseño de la caratula y lista de materiales, para todo aquel que quiera copiarlo.

Pero tengo que retocar algunas cosillas que aun no hace, como responder a la tension de la avionica (para que se apage al quitar el interruptor) o que el retroiluminado se apaque en la posicion off.

Pero mientras he colgado un video donde se ve su funcionamiento actual aprobechando unas teclas que tenia por ahi tiradas.

martes, 24 de enero de 2012

Arduino e IOCP, the easyway.

Arduino es un invento, de eso no cabe duda, y al estar tan extendido es ademas una plataforma muy barata y flexible de programar microcontroladores.
Aunque hay bastante gente que ha conectado su arduino al simulador, hasta ahora lo mas flexible que encontre era el link2fs_inout de Jim. y no es precisame muy flexible o portable, pues parece programado en visual basic y para flight simulator.

No me gustaba, yo queria algo que hablase con sioc, para asi poder utilizarlo indistintamente con x-plane o con FsX

Asi que aplique un poco de filosofia unix, el KISS (keep it simple, stupid).

Mi idea es en el futuro que arduino, con una shield ethernet se comunicase directamente con un servidor IOCP, bien el programa sioc, bien el pluging para xplane, etc,etc...

La cuestion esta en como hacia para que mi actual arduino que se conecta por USB, hablase tambien con sioc.

Estaba claro que la solucion pasaba por un programa que hiciese de traductor entre la conexion TCP que espera IOCP, y la conexion serie que da arduino, que es lo mismo que hace la tarjeta LCD de opencockpits.

Empece programando el Arduino, por dos motivos, ya tengo una protocolo de comunicaciones, el SIOC, es sencillo de entender, y si el dia de mañana adquiero la shield ethernet tengo menos que modificar.

Luego tenia que programar la pasarela, como estoy en windows (por el sioc), y no tengo ni idea, la solucion paso por hacer una warreria en python.

Y voila!!! Funciona, puedo enviar y recibir datos entre arduino y SIOC, es bonito y es flexible.

Por hacer:
Hay algunas cosillas que aun no he terminado:
  • El programa no responde a la orden Arn.Vive
  • Tengo una latencia importante en las comunicaciones, sirve para un piloto automatico, un panel de alarmas, pero no para una plataforma movil, o instrumentos de aguja.He reducido la latencia, aunque no lo he probado con indicadores de aguja u otros indicadores "sensibles"
  • Hay que configurar y compilar algunas cosas a pelo, y hay que isntalar python.

Aqui esta la informacion util:

El programa pasarela.

Hay que instalar python, y luego la libreria pyserial, modificar las primeras lineas del programa para que se ajusten a tu configuracion:
 #!/usr/bin/python            # This is client.py file
# Distributed under GPL V3 License
# (C) Alvaro Alea Fdz 2012
import socket,serial,select # Import socket module

# la configuracion lo primero

host = "127.0.0.1" # socket.gethostname() Get local machine name
port = 8092 # Reserve a port for your service.
serialport=2 # com3

# Conectamos al servidor.
ne = socket.socket() # Create a socket object
print ("Conectando a servidor\n")
ne.connect((host, port))
ne.setblocking(0)
print ("Conectado a s. IOCP\n")
# Abrimos el puerto serie N 9 9600 por defecto
se = serial.Serial(serialport)
se.timeout=0
print ("Abierto puerto serie")
# El loop
#ne.send(bytes("Arn.Inicio:999:\r\n",'UTF-8'))
while 1:

d=0
while d==0:
b= se.read(1)
if b :
ne.send(b)
print (">")
else:
d=1

d=0
while d==0:
ready = select.select([ne],[],[],0.1)
if ready[0]:
a = ne.recv(1)
se.write(a)
print ("<")
else:
d=1
# salida normal del programa.
ne.close() # Close the socket when done
se.close()

El programa de arduino
Es solo el esqueleto de la aplicacion, el resto depende de lo que querais hacer, pronto pondre algun ejemplo completo:
// Distributed under GPL V3 License
// (C) Alvaro Alea Fdz 2012
// Algunas definiciones usadas por claridad.
#define XPDR_CODE 999
int dat,valor;
// llama a esta rutina al principio para iniciar el puerto serie y
// para indicar a sioc que variables monitorizar EDITALO
void setupserial(void){
Serial.begin(9600);
Serial.print("Arn.Inicio:");
Serial.print(XPDR_CODE);
Serial.println(":");
}
// funcion interna, ¿el nº en ascii es una numero?
int isnum(int n){
if ((n>47) && (n<58)) return 1;
else return 0;
}
// llama a esta funcion cuando quieras cambiar algo en sioc UTILIZALA
void enviapares(int dat, int valor){
Serial.print("Arn.Resp:");
Serial.print(dat);
Serial.print("=");
Serial.print(valor);
Serial.println(":");
}
// En esta funcion se reciben todos los cambios de sioc EDITALA
void procesapares(int dat, int valor){
int t1;
if (dat==XPDR_CODE){
}
}
// el meollo de la cuestion, NO TOCAR, FUNCIONA
void serialstuff(void) {
int c=0,pr;
pr=0;
if (Serial.available()==0) return;
c=Serial.read();
do {
switch (p){
case 0:
if (c=='A'){
p=1;
}
pr=1;
break;
case 1:
if (c=='r') {
p=2; pr=1;
} else { p=0;
}
break;
case 2:
if (c=='n') {
p=3; pr=1;
} else { p=0;
}
break;
case 3:
if (c=='.') {
p=4; pr=1;
} else { p=0;
}
break;
case 4:
switch (c) {
case 'R':
p=5; pr=1;
break;
default:
p=0;
break;
}
break;
case 5:
if (c=='e') {
p=6; pr=1;
} else {p=0;
}
break;
case 6:
if (c=='s') {
p=7; pr=1;
} else { p=0;
}
break;
case 7:
if (c=='p') {
p=8; pr=1;
} else { p=0;
}
break;
case 8:
if (c==':'){
p=9; pr=1; dat=0;
} else { p=0; }
break;
case 9:
switch (c){
case 13:
p=20; pr=1;
break;
case '=':
if (dat==0) {
p=0;
} else {
p=10; pr=1;
}
break;
default:
if (isnum(c)) {
dat = 10*dat + (c-48);
pr=1;
} else { p=0; }
break;
}
break;
case 10:
if (isnum(c)){
valor = (c-48); p=11; pr=1;
} else { p=0; }
break;
case 11:
switch (c) {
case ':':
procesapares(dat,valor);
p=8;
break;
default:
if (isnum(c)){
valor = 10*valor + (c-48); pr=1;
} else { p=0; }
break;
}
break;
case 20:
if (c==10) pr=1;
p=0;
break;
default:
p=0; pr=1;
break;
}
} while (pr==0);
}
// Al comenzar el programa inicializa el puerto serie.
void setup(){
setupserial();
}
// El bucle principal.
void loop(){
// Es necesario llamar a esta funciona cada poco para recibir datos.
serialstuff();
}

2012/01/27 - Corregido BUG: Cambiando un writeln por write en la funcion enviapares

viernes, 13 de enero de 2012

¿Merece la pena comprar el panel de radios Saitek?

Los paneles de Saitek estan muy bien (dejando de lado alguna decision de diseño que podriamos discutir.), y son practicamente la unica alternativa comercial, Plug&Play que hay para el simulador de vuelo.
Si tu eres como yo de los que te gusta construirte las cosas, es posible que, como yo, hayas pensado que son carisimos, y que podias hacerte uno por la mitad de precio.
Pues vamos a echar unas cuentas y descubri que, en realidad son regalados de precio, y que hacerte tu algo igual te costaria el doble.
Por supuesto el hacertelo tu tiene la ventaje de poder personalizarlo a gusto o hacerlos mas realistas, o mas comodos de manejar.
Empecemos:
  • 46.25€ incluyendo el descuento de facebook, es lo que te costaria la caratula en policarbonato, cortada en laser, pintada y serigrafiada en hispapanels.
  • 20.00€ Una placa de arduino para controlarlo todo.
  • 42,64€ Dos encoder rotativos de doble eje.
  • 35,18€ Dos mandos concentricos para los encoder
  • 58,95€ Los 4 chips y 20 displays Led de 7 Segmentos rojos de 0.6" de altura
  • 18,67€ En una placa de C.I. botones y algun componente mas.
Total:
  • 221,7€
Es decir justo el doble que en amazon(aunque hay que admitir que no tienen muy claro lo que venden) Y a esto hay que añadirle la programacion del arduino y el programa del Ordenador, y que no tienes soportes, tornillos guapos, ni caja trasera y el tiempo que has tardado en hacerlo.

Para estos calculos he utilizado los precios de la tienda de componentes online Mouser, que esta bastante bien de precio. si alguien esta interesado le puedo pasar el "carrito de la compra" donde desgloso los precios de cada cosa.

Si alguien piensa que los calculos estan mal, y que saitek no puede hacerlo tan barato, hay que tener en cuenta que 90€ se van en "plasticos" y eso a saitek le sale por unos centimos.

Asi que en resumen, esta claro que merecen la pena, son una buena inversion, y ya si te salen como a mi, de segunda mano por 65€ no digamos, Je Je.

Ahora bien, si el frontal te lo hace tu a mano, con metracrilato, sierra y una impresora.
Si en lugar de displays de 7 Segmentos Led utilizas una pantalla retroiluminada de 20x2
Y en lugar de los encoder dobles, utilizas encoders sencillos con interruptor
La cosa cambia mucho, y teniendo la misma funcionalidad y un aspecto muy similar, rondariamos los 50€