Paso 20: Código: agregar Control
En el paso anterior define una rutina de rastreo básico y discutían sobre cómo usar interpolación para mover los servos sin problemas. Ahora, ¿qué pasa si usted quiere controlar el Zombot?
Hay infinidad de maneras de implementar esto, tanto en el hardware y el software. Opté por manejarlo mediante conexión en serie de Arduino, que quiere decir que será realmente trivial para controlar el robot de forma inalámbrica vía Bluetooth, o como lo estoy haciendo en el momento de depurar, simplemente mediante el cable USB.
Esto no es la manera más elegante de alcanzar estos resultados, pero es que fácil de leer y entender, así como flexible.
Como antes de que se atribuyo el código completo más abajo, pero voy a ir a través de los puntos clave aquí.
Protocolo de comunicación
He decidido establecer mi protocolo de comunicación de la siguiente manera.
- Cada paquete de datos (instrucciones) del controlador del robot es una cadena de caracteres que consta de dos partes, separadas por un ":" carácter.
- El "comando" es lo primero y le dice al robot que hacer (por ejemplo: empezar a moverse)
- El "argumento" viene en segundo lugar y proporciona información adicional
- Cada paquete de datos empieza con un "[" y termina con un "]"
Definir Variables globales
Además de las variables previamente definidas, ahora necesitamos configurar algunas variables que se utilizarán en nuestro protocolo de comunicación vía serie.
char inDataCommand[10]; //array to store command char inDataArg[10]; //array to store command argument int inDataIndex = 0; //used when stepping through characters of the packet boolean packetStarted = false; //have we started receiving a data packet (got a "[" boolean argStarted = false; //have we received a ":" and started receiving the arg boolean packetComplete = false; //did we receive a "]" boolean packetArg = false; //are we reading the arg yet, or still the command
Definir las funciones
Un comando de lectura de la serie
Esta función puede ser llamada para comprobar la interfaz en serie para los comandos recibidos.
Comprueba los entrada seriales bytes (caracteres) uno por uno, tirarlas a la basura a menos que sean un "[" que indica el inicio de un comando.
Una vez que se ha iniciado un comando, cada byte se almacena en la variable de "comando" hasta un ":" o "]" se recibe. Si un ":" es recibido, empezamos a almacenar los siguientes bytes en la variable de "argumento" hasta un "]" se recibe.
Si en cualquier momento un "[" se recibe durante la lectura de la instrucción de otra, que la instrucción anterior se descarta. Esto nos impide quedar atascado si alguien nunca transmite un "]" carácter fin de comando y quería enviar un nuevo comando.
Una vez que se ha recibido un comando completo se llama a la función "processCommand", que será realmente interpreto y la acción del comando.
void SerialReadCommand() { /* This function checks the serial interface for incoming commands. It expects the commands to be of the form "[command:arg]" where 'command' and 'arg' are strings of bytes separated by the character ':' and encapsulated by the chars '[' and ']' */ if (Serial.available() > 0) { char inByte = Serial.read();; //incoming byte from serial if (inByte == '[') { packetStarted = true; packetArg = false; inDataIndex = 0; inDataCommand[inDataIndex] = '\0'; //last character in a string must be a null terminator inDataArg[inDataIndex] = '\0'; //last character in a string must be a null terminator } else if (inByte == ']') { packetComplete = true; } else if (inByte == ':') { argStarted = true; inDataIndex = 0; } else if (packetStarted && !argStarted) { inDataCommand[inDataIndex] = inByte; inDataIndex++; inDataCommand[inDataIndex] = '\0'; } else if (packetStarted && argStarted) { inDataArg[inDataIndex] = inByte; inDataIndex++; inDataArg[inDataIndex] = '\0'; } if (packetStarted && packetComplete) { //try and split the packet into command and arg Serial.print("command received: "); Serial.println(inDataCommand); Serial.print("arg received: "); Serial.println(inDataArg); //apply input processUserInput(); packetStarted = false; packetComplete = false; argStarted = false; packetArg = false; } else if (packetComplete) { //this packet was never started packetStarted = false; packetComplete = false; argStarted = false; packetArg = false; } } }
Procesar un comando
Una vez que ha recibido un comando válido (y opcionalmente un argumento), tienen que ser producido, por lo que podemos tomar la acción apropiada.
Por el momento no he necesitado más de un byte de información para definir un comando, por lo que basta con mirar el primer byte de comando. Usando este byte como argumento para un switch() declaración nos permite realizar una función definida por el byte de comando.
En este ejemplo estamos buscando el carácter "w", "s" o "c".
Si se recibe la "w", los cuadros de animación se sobrescriben con una nueva animación que define un movimiento de "mariposa".
Si se recibe la "s", los cuadros de animación se sobrescriben con una nueva animación que define un movimiento de "arrastre".
Si se recibe la "c", los cuadros de animación son todo listo para la misma posición, con eficacia parando todo el movimiento.
Puesto que uno no puede volver a asignar todos los valores en una matriz a la vez, primero definir una nueva matriz temporal para cada servo, que contiene los nuevos cuadros, después utilice "memcpy" para copiar los valores de ubicación de la matriz de marco real en memoria.
void processUserInput() { /*for now all commands are single chars (one byte), can expand later if required char commandByte = inDataCommand[0]; if (commandByte != '\0') { switch (commandByte) { case 'w': { //synchronised forwards (butterfly) numFrames = 4; int newRS[] = {0,1000,1000,0}; int newRE[] = {0,0,1000,1000}; int newLE[] = {0,0,1000,1000}; int newLS[] = {0,1000,1000,0}; int newLN[] = {1000,1000,0,1000}; int newRN[] = {0,1000,1000,1000}; memcpy(crawlGaitRS, newRS,numFrames); memcpy(crawlGaitRE, newRE,numFrames); memcpy(crawlGaitLE, newLE,numFrames); memcpy(crawlGaitLS, newLS,numFrames); memcpy(crawlGaitLN, newLN,numFrames); memcpy(crawlGaitRN, newRN,numFrames); break; } case 's': { //crawl stroke, 180 degrees out of phase numFrames = 4; int newRS[] = {0,1000,1000,0}; int newRE[] = {0,0,1000,1000}; int newLE[] = {1000,0,0,1000}; int newLS[] = {0,0,1000,1000}; int newLN[] = {1000,1000,0,1000}; int newRN[] = {0,1000,1000,1000}; memcpy(crawlGaitRS, newRS,numFrames); memcpy(crawlGaitRE, newRE,numFrames); memcpy(crawlGaitLE, newLE,numFrames); memcpy(crawlGaitLS, newLS,numFrames); memcpy(crawlGaitLN, newLN,numFrames); memcpy(crawlGaitRN, newRN,numFrames); break; } case 'c': { //turn left crawlGaitRS[] = {250,250,250,250}; crawlGaitRE[] = {250,250,250,250}; crawlGaitLE[] = {250,250,250,250}; crawlGaitLS[] = {250,250,250,250}; crawlGaitLN[] = {250,250,250,250}; crawlGaitRN[] = {250,250,250,250}; break; } } } }
Bucle principal
La única adición en el bucle principal es una llamada para verificar la entrada del usuario en cada iteración del bucle.
//get user input using the SerialReadCommand function we wrote SerialReadCommand();