Göra en krypande Robot Zombie med avhuggna ben (20 / 22 steg)
Steg 20: Kod: att lägga till kontroll
I föregående steg vi definierat en grundläggande crawl rutin och diskuterade hur man använder interpolation flytta servon smidigt. Nu, vad händer om du vill styra din Zombot?
Det finns massor av sätt att genomföra detta, både hårdvaran och mjukvaran. Jag valde att hantera den med den Arduino seriell anslutning, vilket innebär att man egentligen trivialt att styra roboten trådlöst via Bluetooth, eller som jag gör just nu för felsökning, helt enkelt via USB-kabeln.
Detta är ingalunda det mest eleganta sättet att uppnå dessa resultat, men det är förhoppningsvis lätt att läsa och förstå, samt flexibla.
Som innan jag kommer att koppla den fullständiga koden nedan, men jag kommer gå igenom de viktigaste punkterna här.
Kommunikationsprotokoll
Jag har beslutat att inrätta min kommunikationsprotokoll på följande sätt.
- Varje datapaket (instruktion) från styrenheten till roboten är en teckensträng som består av två delar, en ":" karaktär.
- "Kommandot" kommer först och berättar roboten vad ska göra (till exempel: börja röra sig)
- "Argumentet" kommer andra och ger extra information
- Varje datapaket börjar med en "[" och slutar med en "]"
Definiera globala variabler
Förutom de tidigare definierade variablerna måste vi nu konfigurera vissa variabler som används i våra kommunikationsprotokoll via följetong.
char inDataCommand [10]. matrisen att lagra kommando
char inDataArg [10]. matrisen att lagra kommandoargument
int inDataIndex = 0; används när du stegade igenom tecken på paketet
booleska packetStarted = false; har vi börjat ta emot ett datapaket (fick en "["
booleska argStarted = false; har vi fått en ":" och börjat ta emot arg
booleska packetComplete = false; fick vi en "]"
booleska packetArg = false; läser vi arg ännu fortfarande kommandot eller
Definiera funktioner
Läs ett kommando från Serial
Denna funktion kan anropas för att kontrollera det seriella gränssnittet för mottagna kommandon.
Den kontrollerar inkommande seriell byte (tecken) taget, kasta bort dem om de inte är en "[" som anger början av ett kommando.
När ett kommando har startat, varje byte lagras i variabeln "kommando" till en ":" eller "]" tas emot. Om en ":" är emot, vi börja lagra följande byte till variabeln "argument" till en "]" tas emot.
Om på någon punkt en "[" tas emot under läsningen av en annan instruktion, att tidigare instruktion ignoreras. Det hindrar oss att fastna om någon aldrig översänds en "]" end-of-kommandot karaktär och vi ville skicka ett nytt kommando.
När en full kommandot har tagits emot "processCommand" funktionen anropas, som kommer att faktiskt interperet och åtgärd kommandot.
void SerialReadCommand() {
/*
Denna funktion kontrollerar det seriella gränssnittet för inkommande kommandon.
Det förväntar sig kommandon till ha formatet "[kommando: arg]"
där "kommando" och "arg" är strängar byte åtskilda av den
tecknet ':' och inkapslat av tecken "[" och "]"
*/
om (Serial.available() > 0) {
char inByte = Serial.read(); inkommande byte från serial
om (inByte == ' [') {
packetStarted = sant;
packetArg = false;
inDataIndex = 0;
inDataCommand [inDataIndex] = '\0'; sista tecknet i en sträng måste vara en null terminator
inDataArg [inDataIndex] = '\0'; sista tecknet i en sträng måste vara en null terminator
}
annars om (inByte == ']') {
packetComplete = sant;
}
annars om (inByte == ':') {
argStarted = sant;
inDataIndex = 0;
}
annars om (packetStarted & &! argStarted) {
inDataCommand [inDataIndex] = inByte;
inDataIndex ++;
inDataCommand [inDataIndex] = '\0';
}
annars om (packetStarted & & argStarted) {
inDataArg [inDataIndex] = inByte;
inDataIndex ++;
inDataArg [inDataIndex] = '\0';
}
om (packetStarted & & packetComplete) {
prova och dela paketet i kommandot och arg
Serial.Print ("kommando fick:");
Serial.println(inDataCommand);
Serial.Print ("arg fick:");
Serial.println(inDataArg);
tillämpa input
processUserInput();
packetStarted = false;
packetComplete = false;
argStarted = false;
packetArg = false;
}
annars om (packetComplete) {
Detta paket var aldrig startat
packetStarted = false;
packetComplete = false;
argStarted = false;
packetArg = false;
}
}
}
Bearbeta ett kommando
När du har tagit emot ett giltigt kommando (och eventuellt ett argument), måste de vara processuppföljning, så att vi kan vidta lämpliga åtgärder.
För tillfället har jag inte behövde mer än en byte för att definiera ett kommando, så vi bara titta på den första byten i kommandot. Med detta byte som argument för ett switch() tillåter uttalande oss att utföra en funktion som definieras av kommandobyte.
I detta exempel söker vi karaktär "w", "s" eller "c".
Om "w" tas emot, skrivs då animeringsbildrutorna över med en ny animering som definierar en "butterfly" rörelse.
Om det "s", skrivs sedan animeringsbildrutorna över med en ny animering som definierar en "krypa" rörelse.
Om "c" tas emot, då är animeringsbildrutorna redo att samma position, effektivt stoppa alla rörelser.
Eftersom man inte kan re-tilldela alla värden i en matris på en gång, vi först definiera en ny tillfällig array för varje servo, som innehåller de nya bildrutorna och sedan använda "memcpy" för att kopiera dessa värden över faktiska ram matrisens plats i minnet.
void processUserInput() {
/ * för alla kommandon är nu enda tecken (en byte), kan expandera senare om det behövs
char commandByte = inDataCommand [0];
om (commandByte! = '\0') {
växel (commandByte) {
fallet "w": {
synkroniserad framlänges (fjäril)
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);
bryta;
}
fallet ": {
crawl stroke, 180 grader ur fas
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);
bryta;
}
fallet "c": {
Sväng vänster
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}.
bryta;
}
}
}
}
Huvudloop
Det enda tillägget krävs i de viktigaste loopen är en uppmaning att kontrollera indata från användaren för varje iteration av loopen.
får indata från användare med hjälp av funktionen SerialReadCommand vi skrev
SerialReadCommand();