Problemas escribiendo en el puerto serie

Hola a todos!

Llevo un tiempo trabajando con Arduino, programando distintos componentes. Pero hay una cosa que nunca me había dado por hacer, y era utilizar únicamente el puerto serie, por ejemplo, para hacer una calculadora software. Este ejemplo lo he hecho sin problemas usando un compilador de C y utilizando la función scanf() para obtener el dato del teclado y asignarlo a una variable de esta forma:
variable = scanf().
Cuando utilizo esta función en C, el programa espera a que yo escriba el dato necesario y que pulse ENTER. Después, continúa con el programa.
El problema que estoy teniendo aquí, es que no consigo hacer que el programa espere a que yo introduzca el valor, sigue con el bucle una y otra vez, y no sé cómo hacer que espere a que le ponga datos por teclado. Os pongo el sketch que estoy programando:

/*
 * Se va a crear un programa que funcione como una calculadora. Tendrá las 
 * opciones estándar de sumar, restar, multiplicar y dividir. Se utilizarán
 * algunas de las estructuras estudiadas anteriormente para mejorar su comprensión
 * y valores enteros para realizar dichas operaciones
 * 
 * Autor: Daniel Lozano Equisoain
 * Fecha: 28/07/2016
 * 
 */

void setup() {
  // se configura el puerto serie para trabajar a 9600 bps
  Serial.begin(9600);
}

void loop() {
  //Inicializo las variables
  int operando1 = 0, operando2 = 0, operacion = 0;
  float resultado = 0;
  
  //Se introducen los números con los que queremos operar
  Serial.println("***** Bienvenido a tu calculadora personal de números enteros *****");
  Serial.println("Introduce los valores para operar:");
  Serial.println("Operando 1:");
  if(Serial.available() > 0){
    operando1 = Serial.read();
  }
  Serial.println("Operando 2:");
  if(Serial.available() > 0){
    operando2 = Serial.read();
  }

  //Se imprimen las opciones con las que se puede operar y se obtiene la opción con la instrucción Serial.read()
  Serial.println("Selecciona la operación a realizar:");
  Serial.println("1) Sumar");
  Serial.println("2) Restar");
  Serial.println("3) Multiplicar");
  Serial.println("4) Dividir");
  Serial.println("Opción: ");

  if(Serial.available() > 0){
    operacion = Serial.read();
  }

  //En función de la opción seleccionada, se realiza una de las siguientes acciones
  switch (operacion){
      case '1': resultado = operando1 + operando2;
                break;
      case '2': resultado = operando1 - operando2;
                break;
      case '3': resultado = operando1 * operando2;
                break;
      case '4': if (operando2 = 0){
                  Serial.println("No se puede realizar la división con 0 como divisor");
                  break;
                } 
                else{
                   resultado = operando1 / operando2;
                   break;
                }
  }

  //Se imprime el resultado final
  Serial.print("Resultado final = ");
  Serial.println(resultado);
  
}

Como veis, es una calculadora hecha a lo bruto, simplemente por ver si consigo hacer funcionar esto en el terminal de Arduino.
He visto en el manual de Arduino que la instrucción if(Serial.available() > 0) se encarga de hacer que el código espere a que yo introduzca o los operandos o la operación, pero ná de ná...

Lo que obtengo son los println() una y otra vez, no se para. ¿Estoy haciendo algo mal? (Muy probablemente..)
He visto ejemplos y códigos en otros foros, pero suelen ser más complejos, no llegan a algo tan básico como esto que estoy comentando de hacer una parada esperando a que se le introduzca algo por teclado.

He compilado para ver cómo funcionaba el código que Arduino pone en su manual, en concreto este:

int incomingByte = 0;   // for incoming serial data

void setup() {
        Serial.begin(9600);     // opens serial port, sets data rate to 9600 bps
}

void loop() {

        // send data only when you receive data:
        if (Serial.available() > 0) {
                // read the incoming byte:
                incomingByte = Serial.read();

                // say what you got:
                Serial.print("I received: ");
                Serial.println(incomingByte, DEC);
        }
}

Pero cuando pongo 5 y le doy a ENTER, en vez de devolverme "I received: 5", recibo esto:
I received: 53
I received: 13
I received: 10

...Todo de golpe...

Os agradecería cualquier tipo de ayuda. Que me salgan mal cosas tan básicas me trae por el camino de la amargura...

Muchas gracias a todos!

Una pena que luego de plantear correctamente todo el hilo no te hayas percatado que lo hiciste en la sección en la que no se debe postear.
Movido a Software.

A prestar atención. La sección esta llena de hilos MOVIDOS
Parece que el 100% de los nuevos NO VEN o NO LES IMPORTA 15 mensajes

Esto va para los nuevos que lean este hilo
1er hilo: No usar esta sección del Foro
2do Sobre escribir todo en mayúsculas

MOVED
MOVED
MOVED
MOVED
MOVED
MOVED
MOVED
MOVED
MOVED
MOVED
MOVED
MOVED

Prueba esto. Las modificaciones están comentadas :slight_smile:

/*
   Se va a crear un programa que funcione como una calculadora. Tendrá las
   opciones estándar de sumar, restar, multiplicar y dividir. Se utilizarán
   algunas de las estructuras estudiadas anteriormente para mejorar su comprensión
   y valores enteros para realizar dichas operaciones

   Autor: Daniel Lozano Equisoain
   Fecha: 28/07/2016

*/

void setup() {
  // se configura el puerto serie para trabajar a 9600 bps
  Serial.begin(9600);
}

void loop() {
  //Inicializo las variables
  int operando1 = 0, operando2 = 0, operacion = 0;
  float resultado = 0;
  boolean operacionValida = true; // Sirve para validar la operación ingresada.

  //Se introducen los números con los que queremos operar
  Serial.println("***** Bienvenido a tu calculadora personal de números enteros *****");
  Serial.println("Introduce los valores para operar:");
  Serial.println("Operando 1:");

  while (!Serial.available()) {} // No hace nada hasta que llegue algo
  delay(5); // Una pequeña espera para recibir todo.
  // operando1 = Serial.read(); NO
  operando1 = Serial.parseInt(); // Sí
  Serial.println("Operando 2:");

  while (!Serial.available()) {} // No hace nada hasta que llegue algo
  delay(5); // Una pequeña espera para recibir todo.
  // operando2 = Serial.read(); NO
  operando2 = Serial.parseInt(); // Sí

  //Se imprimen las opciones con las que se puede operar y se obtiene la opción con la instrucción Serial.read()
  Serial.println("Selecciona la operación a realizar:");
  Serial.println("1) Sumar");
  Serial.println("2) Restar");
  Serial.println("3) Multiplicar");
  Serial.println("4) Dividir");
  Serial.println("Opción: ");
  Serial.println(); // Añade una línea en blanco adicional, para visualizar mejor los resultados

  while (!Serial.available()) {} // No hace nada hasta que llegue algo
  operacion = Serial.read();

  //En función de la opción seleccionada, se realiza una de las siguientes acciones
  do {
    switch (operacion) {
      case '1': resultado = operando1 + operando2;
        operacionValida = true;
        break;
      case '2': resultado = operando1 - operando2;
        operacionValida = true;
        break;
      case '3': resultado = operando1 * operando2;
        operacionValida = true;
        break;
      case '4': if (operando2 == 0) { // Es == , no =
          Serial.println("No se puede realizar la división con 0 como divisor");
          // break; Basta con ponerlo una vez después de este if-else
        }
        else {
          resultado = operando1 / operando2;
          // break; Basta con ponerlo una vez después de este if-else
        }
        operacionValida = true;
        break;

      default: operacionValida = false;
        Serial.println("Opción inválida. Por favor, elija una de las siguientes:");
        Serial.println("1) Sumar");
        Serial.println("2) Restar");
        Serial.println("3) Multiplicar");
        Serial.println("4) Dividir");
        Serial.println(); // Añade una línea en blanco adicional, para visualizar mejor los resultados
        while (!Serial.available()) {} // No hace nada hasta que llegue algo
        operacion = Serial.read();
    }
  } while (!operacionValida); // Sigue pidiendo hasta que el usuario dé un dato válido.

  //Se imprime el resultado final
  Serial.print("Resultado final = ");
  Serial.println(resultado);
  delay(2000); // Retrasar un poco para que el mensaje de bienvenida no se imprima repentinamente
  Serial.println();
  Serial.println(); // Dos líneas en blanco, para demarcar la repetición del programa

}

Uno de los problemas que he encontrado (recurrente por cierto), es que a veces confunden datos binarios con texto; o quizá desconocen las funciones read y los "parsers".
Por qué digo esto? Porque Serial.read sólo lee un byte/caracter; y lo que ingreses al monitor serie siempre será texto.

Para que lo entiendas así:

  • Dato binario: como lo almacena una variable byte, char, int, long, float.
  • Texto: secuencia de caracteres (char o byte) codificados según la tabla ASCII (y el alfabeto o "charset" del idioma).

Como no son la misma cosa, hay que recurrir a las "funciones conversoras", los cuales son:

Binario a texto: esos son los "printers" (sprintf, print, println).
Texto a binario (lo que necesitas): esos son los "parsers" (atoi, atol, atof, parseInt, parseFloat, toInt, toFloat).

Perdóname por la larga historia, solo espero haberme explicado bien... ::slight_smile:

Te has explicado de lujo!! Muchas gracias!

Como ahora estoy trabajando no lo puedo probar, pero en cuanto llegue a casa lo voy a probar a ver que tal funciona.
Permíteme preguntarte un par de dudas que me surgen:

  1. Tanto para el operando1, como para el operando2, para recoger el valor del teclado utilizas Serial.parseInt(), pero para el operador, que también será un número, utilizas Serial.read(). ¿Por qué no se utilizaría también parseInt() en ese caso?

El operador lo seleccionaré con un número, teniendo que teclear "1" para seleccionar la primera opción de suma por ejemplo, o "2" para seleccionar la opción de restar.

  1. Para hacer que el programa no haga nada hasta que yo envíe una orden, utilizas while (!Serial.available()) {}. ¿Por qué en el ejemplo de la ayuda de Arduino utiliza if(Serial.available()>0)? Esta instrucción que pone Arduino en su ejemplo a mí no me funciona. ¿Puede deberse a que yo no la esté usando bien, o que se utilice para otro propósito?

Repito, muchísimas gracias por la ayuda. Esta tarde en cuanto llegue a casa lo voy a probar y te cuento los resultados.

Por otro lado quiero pedirte disculpas surbyte. He leído tu mensaje y tienes razón, no he leído el post para saber dónde tenía que escribir. No pasará la próxima vez!

En breves os cuento que tal han ido los cambios!!

lozi_dani:

  1. Tanto para el operando1, como para el operando2, para recoger el valor del teclado utilizas Serial.parseInt(), pero para el operador, que también será un número, utilizas Serial.read(). ¿Por qué no se utilizaría también parseInt() en ese caso?

El operador lo seleccionaré con un número, teniendo que teclear "1" para seleccionar la primera opción de suma por ejemplo, o "2" para seleccionar la opción de restar.

De nuevo, recuerda que lo que entra al monitor serie, es meramente texto; y además hay algo que había dicho antes:

Lucario448:
Serial.read sólo lee un byte/caracter

Y qué es lo que recibe el programa para decidir la operación? Un caracter.

Por otra parte, nótese que los números de cada case están entre comillas simples, eso quiere decir que está comparando un caracter ASCII con otro; por eso ahí el dato se procesa como texto, no como un valor de variable (dato binario).

Por ejemplo: en un contexto matemático, '1' (caracter) no es igual a 1 (valor), sino a 49. Eso quiere decir que:

case '1':
case 49:
// Son exactamente lo mismo

Cuando envías ese número desde el monitor serie, en realidad estás enviando un byte/char que vale 49.

lozi_dani:
2) Para hacer que el programa no haga nada hasta que yo envíe una orden, utilizas while (!Serial.available()) {}. ¿Por qué en el ejemplo de la ayuda de Arduino utiliza if(Serial.available()>0)? Esta instrucción que pone Arduino en su ejemplo a mí no me funciona. ¿Puede deberse a que yo no la esté usando bien, o que se utilice para otro propósito?

Serial.available devuelve la cantidad (numérica) de bytes disponibles para "recojer" del búfer. Las funciones que "recojen" esos bytes, son todas los que empecen en "read".
Por "recojer" se entiende a que una vez hecho eso, el byte desaparece del búfer (memoria de almacenamiento temporal).

Ya sabiendo lo anterior, podremos deducir que si devuelve un cero, es que no hay nada.

En un contexto booleano (verdadero/falso), es válido colocar cualquier tipo de dato (excepto el float, porque desconozco cómo se interpreta); debido a que se sigue esta regla:

Si es igual a cero, entonces es false; de lo contrario, es true.

Otro punto que cabe destacar, es el símbolo '!'; el operador booleano NOT (negación). El resultado es el valor opuesto del operando (true a false y viceversa).

Combinando estos tres conocimientos, es que obtenemos esta línea de código:

while (!Serial.available()) {}

Voy a explicarlo por pasos:

  • Supongamos que no hay nada en búfer del serial (no ha recibido nada todavía), entonces Serial.available devolverá cero.
  • Como el resultado de la función es un cero, y estamos en un contexto booleano; cero equivale a false.
  • Pero nótese que tenemos también un '!', entonces hay que invertir el valor de lo que tenga a su derecha; en este caso, false. Y como el opuesto de eso es true, entonces ese es el resultado.
    De todo esto podemos concluir que la interpretación final de esa línea, es la siguiente:

Mientras el búfer del serial esté vacío, no hacer nada.

Y así, mi querido amigo, es como el programa espera a que el usuario digite algo. :slight_smile:

Respondiendo a la otra pregunta:

En el ejemplo que viene en la IDE, el loop solo imprime algo si hay bytes en el búfer; de lo contrario no hace nada.
El código que tu posteaste al principio, el "flujo de instrucciones" del loop hacía que imprimera el menú principal; independientemente de que si recibía datos o no. Y por ser una función de ciclo infinito, pues obviamente tal acción se iba a repetir indefinidamente.
Una setencia if no detendrá el programa aunque su condición no se cumpla.

Por último (por fiiiin):

if (Serial.available() > 0)

Escribirlo así es una forma más fácil de entender para un principiante. Sin embargo, equivale a esto:

if (Serial.available())

Por no llevar '!', se interpreta al contrario de lo que expliqué anteriormente:

Si ("Mientras" si fuera un while) hay algo en el búfer, hacer...

Esto último no aplica sólo a casos muy específicos (y raros), como por ejemplo al leer un archivo de una tarjeta SD en una posición fuera de este (available devolvería un número negativo, el cual no es cero; por lo tanto sigue siendo true).
Dicho en otras palabras, solo ocurría en un inusual fallo de programación.

Ufff, espero que hayas podido con tanta información; y más yo que fui el que la escribió... ::slight_smile:

Uau!! Tremenda respuesta! Muy clara, muchas gracias!

Ahora sí entiendo la diferencia en el comportamiento del while y el if con el Serial.available(). Tiene sentido viendo el razonamiento la verdad... torpe de mi de no haber caído en eso.

Acabo de hacer las pruebas con las modificaciones en el código que me has sugerido. Obtengo lo siguiente:

(esto es lo que sale en el terminal, lo pongo como código para separarlo del texto plano jeje)

***** Bienvenido a tu calculadora personal de números enteros *****
Introduce los valores para operar:
Operando 1:1
Operando 2:0
Selecciona la operación a realizar:
1) Sumar
2) Restar
3) Multiplicar
4) Dividir
Opción: 

Resultado final = 1.00


***** Bienvenido a tu calculadora personal de números enteros *****
Introduce los valores para operar:
Operando 1:0
Operando 2:

El programa ha llegado hasta ahí de la siguiente forma:

  • Primero me aparece esto:
***** Bienvenido a tu calculadora personal de números enteros *****
Introduce los valores para operar:
Operando 1:
  • Inserto el valor 1 por ejemplo, pulso intro y sucede esto:
***** Bienvenido a tu calculadora personal de números enteros *****
Introduce los valores para operar:
Operando 1:1
Operando 2:0
Selecciona la operación a realizar:
1) Sumar
2) Restar
3) Multiplicar
4) Dividir
Opción:

Se puede ver cómo en el operando2 introduce automáticamente un 0, sin introducir yo ningún valor para el operando2.

  • Como pide la opción, pulso 1 para la suma, por ejemplo, y obtengo esto:
***** Bienvenido a tu calculadora personal de números enteros *****
Introduce los valores para operar:
Operando 1:1
Operando 2:0
Selecciona la operación a realizar:
1) Sumar
2) Restar
3) Multiplicar
4) Dividir
Opción: 

Resultado final = 1.00


***** Bienvenido a tu calculadora personal de números enteros *****
Introduce los valores para operar:
Operando 1:0
Operando 2:

Me devuelve el resultado final de la suma 1 (que yo he metido) + 0 (que se ha puesto solo). Luego me saca otra vez el mensaje de Bienvenido... y al introducir los valores, mete en el operando1 automáticamente el valor 0, y me pide que meta el valor para el operando2.

A partir de ahí el programa hace cosas raras de poner el menú de opciones varias veces diciéndome que la opción seleccionada no es válida cuando no meto ningún valor más.
Creo que por algún lado está metiendo ese valor 0 pero no termino de ver por dónde ni por qué.

¿Se te ocurre alguna razón? Me resulta un tanto extraño.. :o

De nuevo muchas gracias por la ayuda! Me encanta este foro porque no te dicen cómo hacerlo, sino que la gente ayuda a entender el por qué se hacen las cosas =)

Presta atención porque viene otra explicación larga!!! :o

Acabo de probar el código, y a mi no se me envía nada por sí solo. Hay dos posibilidades:

  • El monitor serie tiene algún "Ajuste de línea"; esto hace que se envíen caracteres de más.
  • Digitar datos no válidos después del número.

Puede estar ocurriendo lo siguiente:

Supongamos que está ocurriendo lo del primer punto (que es lo más probable).

parseInt devuelve lo que válidamente alcance analizar, y/o hasta que el timeout se agote (por defecto dura 1 segundo).
Por lo que veo, se detiene hasta toparse con un caracter inválido; provocando sobrantes en el búfer. Esas sobras son las que inutilizan la espera del operando 2 (RECUERDA QUE LOS while FUNCIONAN SÓLO SI EL BÚFER ESTÁ VACÍO).

Como en el operando 2 se pasa directo al parseInt; este intenta analizar la secuencia de caracteres sobrante, la cual no es válida. Y al no ser un dato válido, esta programado a devolver cero en caso de fallar (por defecto). De ahí el cero "fantasma".

Luego el búfer queda vacío, por eso se detiene en solicitar la operación.
Cuando intentas enviar algo otra vez, volvemos a tener de nuevo los sobrantes. De ahí que al reinicio ahora sea el operando 1 el que reciba el "cero fantasma".

En ese segundo intento, es posible que el selector de operaciones hubiera recibido un '\r' o un '\n' (valen 10 y 13 respectivamente). Esos son los posibles susodichos "caracteres sobrantes" (y que textualmente no son números, sino marcas que indican que se debe crear una nueva línea).

Soluciones:

  • Verifica que el monitor serie esté "sin ajuste de línea", así no enviará caracteres de más cada vez que se intente.
  • Colocar la función flush después de cada lectura. Por ejemplo:
while (!Serial.available()) {} // No hace nada hasta que llegue algo
  delay(5); // Una pequeña espera para recibir todo.
  // operando1 = Serial.read(); NO
  operando1 = Serial.parseInt(); // Sí
  Serial.flush(); // Nueva sentencia a colocar después de cada read o parseInt

La función de la función (valga la redundancia) flush puede variar según la librería que la utilice; sin embargo, todas tienen una en común: vaciar su respectivo búfer.

En el caso de Serial, sirve para dos cosas dependiendo de la situación:

Bloquea el programa hasta que el "bloque" (conjunto) de bytes termine de enviarse por completo (si está en ese proceso).
Si no está enviando nada, vacía el búfer descartando todo lo que haya en él. Esto provocará que available retorne cero y que cualquier sucesiva lectura devuelva su valor por defecto (read devolvería -1; y parseInt cero).

Yyyyyyyy... Eso es todo amigos! :smiley:

Como bien has dicho, el problema por el que me cogía caracteres "fantasma" era por el ajuste de línea. La explicación ha sido perfecta y me ha ayudado a entender muchas cosas, de verdad. Por fin el programa funciona poniendo la opción "Sin ajuste de línea".

Permíteme preguntarte una cosa acerca de ese pequeño menú de opciones donde se encuentra lo del "Sin ajuste de línea".

Hice un programa utilizando un Arduino Nano y el módulo WiFi ESP8266-01 para manejar los motores de un pequeño robot a través de un interfaz web. En ese caso, tuve que poner como opción en ese pequeño menú la que dice "Ambos NL & CR". En su momento, en un hilo del foro github, me recomendaron que para ese tipo de programas era recomendable utilizar esta última opción que te digo, pero no termino de entender el uso de las funciones que se despliegan. Ahora usando "Sin ajuste de línea" funciona bien, mientras que en el otro caso, con "Ambos NL & CR", también funcionaba bien.

¿Podrías indicarme un poco la función de cada opción o quizás redirigirme a algún sitio donde lo expliquen? Tampoco quiero explotarte con mis dudas jeje, pero me viene de lujo conocer cómo se deben de configurar las opciones en este entorno para que no me pasen fallos como este.

De nuevo muchas gracias por las explicaciones anteriores, gracias a tu ayuda me funciona el programa perfectamente y he podido entender muchas cosas! :slight_smile:

Bueno... no sé si lo que me preguntas es lo del ESP8266 o los ajustes de línea, pero de todas formas responderé a ambas cosas.

Los ajustes de línea: algunos dispositivos/sistemas que se comunican por puerto serial, requieren de estos caracteres más que todo como "separadores" o "terminadores" de una secuencia o "bloque" de bytes (ya sea por cuestiones de ordenamiento, formato; o por carecer de algún mecanismo que lleve un registro del búfer, algo tipo Serial.available). Las opciones disponibles hacen lo siguiente:

  • Sin ajuste de línea: envía solamente lo escrito en el monitor serie, sin nada adicional.
  • Nueva línea: aparte de lo escrito en el monitor serie, también le adjunta al final el caracter '\n'. Algunos programas esperan que el "bloque" acabe con este caracter y no otro; también es el más usado como separador o marca de nueva línea en "loggers".
  • Retorno del carro: adjunta el caracter '\r'. Se suele usar como terminador solamente; para otros usos es inusual.
  • Ambos NL & CR: adjunta la secuencia de caracteres "\r\n" al final (y en ese orden). Se usa en "loggers" como marca de nueva línea, pero esta vez para seguir el formato de "texto plano" de DOS/Windows. Para otros fines... sinceramente lo desconozco.

Sobre el tema con el ESP8266: Como no sé cómo es el código utilizado, voy especular con esta explicación:

Posiblemente la longitud del "bloque de datos" es constante; y el programa sabe lidiar con los "ajustes de línea"; por esta razón, le es indiferente que los tenga o no.

Reitero: debido al desconocimiento del código, esta última explicación no esta fundamentada; y por ende, puede estar errada.