diff --git a/LICENSE b/LICENSE index acbff5a..921921d 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,8 @@ MIT License Copyright (c) 2017 Alistair Lynn, Thomas Leese 2017-2018 Jake Howard, Kier Davis, Peter Law - 2018 Dan Trickey + 2018-2022 Dan Trickey + 2023 SourceBots Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d6b1e86..cdd3b03 100644 --- a/README.md +++ b/README.md @@ -2,31 +2,32 @@ The firmware for SourceBots' Arduino Uno board. -It communicates using commands sent over the USB serial pins. +It communicates using commands sent over USB serial. -Commands consist of a single character, followed by up to two arguments. The command is not separated from the first argument, but the first and second argument are separated by a space. - -Request IDs can be specified by prefacing your command with `@int`, for example `@8335`. This is then returned with the command response and can be used to prevent race conditions. +Commands consist of a string terminated by a newline character (\n). +Commands consist of multiple parts seperated by a colon character. ## Commands -| Command | Description | Parameter 1 | Parameter 2 | -|---------|-----------------------------|--------------|------------------------| -| A | Read the analogue pins | | | -| L | Control the debug LED | State {H, L} | | -| R | Read a digital pin | Pin Number | | -| S | Control a servo | Servo Number | Width | -| T | Read a triggered pulse delay| Trigger Pin | Echo Pin | -| U | Read an ultrasound distance | Trigger Pin | Echo Pin | -| V | Get the firmware version | | | -| W | Write to a Pin | Pin Number | Pin State {H, L, P, Z} | +| Command | Description | Parameters | +|-----------------------------------|------------------------------|------------------| +| PIN:\:MODE:GET? | Read pin mode | n = pin number | +| PIN:\:MODE:SET:\ | Set pin mode | n = pin number, value=INPUT/INPUT_PULLUP/OUTPUT| +| PIN:\:DIGITAL:GET? | Digital read pin | n = pin number | +| PIN:\:DIGITAL:SET:\ | Digital write pin | n = pin number, value = 1/0 | +| PIN:\:ANALOG:GET? | Analog read pin | n = pin number | +| ULTRASOUND:\:\:MEASURE?| Measure range from ultrasound| pulse = pulse pin, echo = echo pin | ## Example Commands -Read the analogue pins: `A` - -Turn on the debug LED: `LH` - -Read the ultrasound distance: `U4 5` - -Set pin 6 High: `W6 H` +Set pin mode to input and analog read pin +``` +PIN:14:MODE:SET:INPUT +PIN:14:ANALOG:GET? +``` + +Set pin to output and set it high +``` +PIN:2:MODE:SET:OUTPUT +PIN:2:DIGITAL:SET:1 +``` diff --git a/src/firmware.cpp b/src/firmware.cpp deleted file mode 100644 index 10285f0..0000000 --- a/src/firmware.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include - -#include - -typedef String CommandResponse; -static const CommandResponse OK = ""; - -#define COMMAND_ERROR(x) ((x)) - -// Multiplying by this converts round-trip duration in microseconds to distance to object in millimetres. -static const float ULTRASOUND_COEFFICIENT = 1e-6 * 343.0 * 0.5 * 1e3; - -static const String FIRMWARE_VERSION = "SBDuino GPIO v2019.8.0"; - -// Helpful things to process commands. - -class CommandHandler { - public: - char command; - CommandResponse (*run)(int, String argument); - CommandHandler(char cmd, CommandResponse(*runner)(int, String)); -}; - -CommandHandler::CommandHandler(char cmd, CommandResponse (*runner)(int, String)) - : command(cmd), run(runner) -{ - -} - -static String pop_option(String& argument) { - argument.trim(); - - int separatorIndex = argument.indexOf(' '); - if (separatorIndex == -1) { - String copy(argument); - argument = ""; - return copy; - } else { - String first_argument(argument.substring(0, separatorIndex)); - argument = argument.substring(separatorIndex + 1); - return first_argument; - } -} - -static void serialWrite(int requestID, char lineType, const String& str) { - if (requestID != 0) { - Serial.write('@'); - Serial.print(requestID, DEC); - Serial.write(' '); - } - - Serial.write(lineType); - Serial.write(' '); - - Serial.println(str); -} - -// The actual commands - -static void readAnaloguePin(int requestID, const String& name, int pin) { - serialWrite(requestID, '>', name + " " + String(analogRead(pin))); -} - -static CommandResponse analogueRead(int requestID, String argument) { - readAnaloguePin(requestID, "a0", A0); - readAnaloguePin(requestID, "a1", A1); - readAnaloguePin(requestID, "a2", A2); - readAnaloguePin(requestID, "a3", A3); - return OK; -} - -static CommandResponse led(int requestID, String argument) { - pinMode(LED_BUILTIN, OUTPUT); - char state = argument.charAt(0); - - switch (state) { - case 'H': - digitalWrite(LED_BUILTIN, HIGH); - break; - case 'L': - digitalWrite(LED_BUILTIN, LOW); - break; - default: - return COMMAND_ERROR("Unknown LED State: " + argument); - } - - return OK; -} - -static CommandResponse readPin(int requestID, String argument) { - String pinIDArg = pop_option(argument); - - if (argument.length() || !pinIDArg.length()) { - return COMMAND_ERROR("Bad number of arguments"); - } - - int pin = pinIDArg.toInt(); - - if (pin < 2 || pin > 13) { - return COMMAND_ERROR("pin must be between 2 and 13"); - } - - int state = digitalRead(pin); - - if (state == HIGH) { - serialWrite(requestID, '>', "H"); - } else { - serialWrite(requestID, '>', "L"); - } - - return OK; -} - -static CommandResponse servo(int requestID, String argument) { - return COMMAND_ERROR("not implemented"); -} - -static void ultrasoundTriggerIO(int triggerPin, int echoPin){ - // Reset trigger pin. - pinMode(triggerPin, OUTPUT); - digitalWrite(triggerPin, LOW); - delayMicroseconds(2); - - // Pulse trigger pin. - digitalWrite(triggerPin, HIGH); - delayMicroseconds(10); - digitalWrite(triggerPin, LOW); - - // Set echo pin to input now (we don't do it earlier, since it's allowable - // for triggerPin and echoPin to be the same pin). - pinMode(echoPin, INPUT); -} - -static CommandResponse ultrasoundReadTiming(int requestID, String argument){ - String triggerPinStr = pop_option(argument); - String echoPinStr = pop_option(argument); - - if (argument.length() || !triggerPinStr.length() || !echoPinStr.length()) { - return COMMAND_ERROR("Arguments required: "); - } - - int triggerPin = triggerPinStr.toInt(); - int echoPin = echoPinStr.toInt(); - - if (triggerPin < 2 || triggerPin > 13 || echoPin < 2 || echoPin > 13) { - return COMMAND_ERROR("Pins must be between 2 and 13"); - } - - ultrasoundTriggerIO(triggerPin, echoPin); - - // Read return pulse. - float duration = (float) pulseIn(echoPin, HIGH); // In microseconds. - - serialWrite(requestID, '>', String(duration)); - - return OK; -} - -static CommandResponse ultrasoundReadDistance(int requestID, String argument) { - String triggerPinStr = pop_option(argument); - String echoPinStr = pop_option(argument); - - if (argument.length() || !triggerPinStr.length() || !echoPinStr.length()) { - return COMMAND_ERROR("Arguments required: "); - } - - int triggerPin = triggerPinStr.toInt(); - int echoPin = echoPinStr.toInt(); - - if (triggerPin < 2 || triggerPin > 13 || echoPin < 2 || echoPin > 13) { - return COMMAND_ERROR("Pins must be between 2 and 13"); - } - - ultrasoundTriggerIO(triggerPin, echoPin); - - // Read return pulse. - float duration = (float) pulseIn(echoPin, HIGH); // In microseconds. - float distance = duration * ULTRASOUND_COEFFICIENT; // In millimetres. - distance = constrain(distance, 0.0, (float) UINT_MAX); // Ensure that the next line won't overflow. - unsigned int distanceInt = (unsigned int) distance; - - serialWrite(requestID, '>', String(distanceInt)); - - return OK; -} - -static CommandResponse version(int requestID, String argument) { - serialWrite(requestID, '>', FIRMWARE_VERSION); - return OK; -} - -static CommandResponse writePin(int requestID, String argument) { - String pinIDArg = pop_option(argument); - String pinStateArg = pop_option(argument); - - if (argument.length() || !pinIDArg.length() || !pinStateArg.length()) { - return COMMAND_ERROR("Arguments Needed: "); - } - - int pin = pinIDArg.toInt(); - - if (pin < 2 || pin > 13) { - return COMMAND_ERROR("pin must be between 2 and 13"); - } - - switch (pinStateArg.charAt(0)) { - case 'H': - pinMode(pin, OUTPUT); - digitalWrite(pin, HIGH); - break; - case 'L': - pinMode(pin, OUTPUT); - digitalWrite(pin, LOW); - break; - case 'P': - pinMode(pin, INPUT_PULLUP); - break; - case 'Z': - pinMode(pin, INPUT); // High Impedance - break; - default: - return COMMAND_ERROR("Unknown pin mode"); - - } - return OK; -} - -// Process the commands and execute them. - -static const CommandHandler commands[] = { - CommandHandler('A', &analogueRead), // Read the analogue pins - CommandHandler('L', &led), // Control the debug LED (H/L) - CommandHandler('R', &readPin), // Read a digital pin - CommandHandler('S', &servo), // Control a servo - CommandHandler('T', &ultrasoundReadTiming), // Read an ultrasound raw timing - CommandHandler('U', &ultrasoundReadDistance), // Read an ultrasound distance - CommandHandler('V', &version), // Get firmware version - CommandHandler('W', &writePin), // Write to or a GPIO pin -}; - -static void dispatch_command(int requestID, const class CommandHandler& handler, const String& argument) { - auto response = handler.run(requestID, argument); - if (response == OK) { - serialWrite(requestID, '+', "OK"); - } else { - serialWrite(requestID, '-', "Error: " + response); - } -} - -static void handle_actual_command(int requestID, const String& cmd) { - char commandIssued = cmd.charAt(0); - - for (int i = 0; i < sizeof(commands) / sizeof(CommandHandler); ++i) { - const CommandHandler& handler = commands[i]; - - if (handler.command == commandIssued) { - dispatch_command(requestID, handler, cmd.substring(1)); - return; - } - } - - serialWrite(requestID, '-', String("Error, unknown command: ") + commandIssued); -} - -static void handle_command(const String& cmd) { - // A command is prefixed with @ if an identifier is used to prevent race conditions - if (cmd.startsWith("@")) { - auto spaceIndex = cmd.indexOf(' '); - auto requestID = cmd.substring(1, spaceIndex).toInt(); - handle_actual_command(requestID, cmd.substring(spaceIndex + 1)); - } else { - handle_actual_command(0, cmd); - } -} - -static String serialBuffer; -static boolean skipWS = false; - -static void process_serial() { - auto serialInput = Serial.read(); - - // Allow resetting the buffer by sending a NULL. This allows for recovery - // from partial commands being sent or received. - if (serialInput == 0) { - serialBuffer = ""; - return; - } - - if (serialInput == -1) { - return; - } - - if (serialInput == '\r') { - return; // ignore CR, just take the LF - } - - if (serialInput == '\t') { - serialInput = ' '; // treat tabs as equivalent to spaces - } - - if (serialInput == '\n') { - serialBuffer.trim(); - Serial.write("# "); - Serial.write(serialBuffer.c_str()); - Serial.write('\n'); - handle_command(serialBuffer); - serialBuffer = ""; - Serial.flush(); - return; - } - - if (serialInput == ' ' && skipWS) { - return; // ignore junk whitespace - } else { - skipWS = (serialInput == ' '); // ignore any successive whitespace - } - - serialBuffer += (char)serialInput; -} - - -void setup() { - // Setup the pins. - pinMode(LED_BUILTIN, OUTPUT); - for (int pin = 2; pin <= 13; ++pin) { - pinMode(pin, INPUT); - } - - Serial.begin(115200); - Serial.setTimeout(5); - - Serial.write("# Booted\n"); - serialWrite(0, '#', FIRMWARE_VERSION); -} - -void loop() { - while (Serial.available()) { - process_serial(); - } -} diff --git a/src/src.ino b/src/src.ino index e69de29..55c4216 100644 --- a/src/src.ino +++ b/src/src.ino @@ -0,0 +1,208 @@ +String serial_line = ""; +String current_arg = ""; + +void setup() { + Serial.begin(115200); +} + +void loop(){ + if (Serial.available() > 0) { + // Read characters one at a time into a buffer + // process the string as a command when we receive a newline + char c = Serial.read(); + if (c == '\n') { + processCommand(); + cleanUp(); + } + else { + serial_line += c; + } + } +} + +// All comands have multiple segments seperated by colons +// current_arg becomes the next input segment +// serial_line is modifed to remove the start segment +void getSlice() { + int pos = serial_line.indexOf(":"); + if (pos == -1) { + current_arg = serial_line; + serial_line = ""; + } + else { + // Current arg is the bit upto the colon; + current_arg = serial_line.substring(0, pos); + // serial line becomes the remaining bit; + serial_line = serial_line.substring(pos + 1); + } +} + +// Used to cleanup the input buffer after a command has been processed +void cleanUp() { + serial_line = ""; +} + +// Take the input buffer and process the command +void processCommand() { + getSlice(); + + if (current_arg.equals("*IDN?")) { + // CMD: *IDN? + Serial.print("SourceBots:Arduino:X:2.0\n"); + return; + } + + else if (current_arg.equals("*STATUS?")) { + // CMD: *STATUS? + Serial.print("Yes\n"); + return; + } + + else if (current_arg.equals("*RESET?")) { + // CMD: *RESET? + Serial.print("NACK:Reset not supported\n"); + return; + } + + else if (current_arg.equals("PIN")) { + getSlice(); + int pin = current_arg.toInt(); + getSlice(); + + if (current_arg.equals("MODE")) { + getSlice(); + if (current_arg.equals("GET?")) { + // CMD: PIN::MODE:GET? + int mode = getPinMode(pin); + if (mode == INPUT) { + Serial.print("INPUT\n"); + return; + } + else if (mode == INPUT_PULLUP) { + Serial.print("INPUT_PULLUP\n"); + return; + } + else if (mode == OUTPUT) { + Serial.print("OUTPUT\n"); + return; + } + } + else if (current_arg.equals("SET")) { + getSlice(); + if (current_arg.equals("INPUT")) { + // CMD: PIN::MODE:SET:INPUT + pinMode(pin, INPUT); + Serial.print("ACK\n"); + return; + } + else if (current_arg.equals("INPUT_PULLUP")) { + // CMD: PIN::MODE:SET:INPUT_PULLUP + pinMode(pin, INPUT_PULLUP); + Serial.print("ACK\n"); + return; + } + else if (current_arg.equals("OUTPUT")) { + // CMD: PIN::MODE:SET:OUTPUT + pinMode(pin, OUTPUT); + Serial.print("ACK\n"); + return; + } + } + } + + else if (current_arg.equals("DIGITAL")) { + getSlice(); + if (current_arg.equals("GET?")) { + // CMD: PIN::DIGITAL:GET? + if (digitalRead(pin)) { + Serial.print("1\n"); + return; + } + else { + Serial.print("0\n"); + return; + } + } + else if (current_arg.equals("SET")) { + getSlice(); + if (current_arg.equals("1")) { + // CMD: PIN::DIGITAL:SET:1 + digitalWrite(pin, HIGH); + Serial.print("ACK\n"); + return; + } + else { + // CMD: PIN::DIGITAL:SET:0 + digitalWrite(pin, LOW); + Serial.print("ACK\n"); + return; + } + } + } + + else if (current_arg.equals("ANALOG")) { + getSlice(); + if (current_arg.equals("GET?")) { + // CMD: PIN::ANALOG:GET? + Serial.print(analogRead(pin)); + Serial.print("\n"); + return; + } + } + } + + else if (current_arg.equals("ULTRASOUND")) { + getSlice(); + int pulse = current_arg.toInt(); + getSlice(); + int echo = current_arg.toInt(); + getSlice(); + if (current_arg.equals("MEASURE?")) { + // CMD: ULTRASOUND:::MEASURE? + + // config pins to correct modes + pinMode(pulse, OUTPUT); + pinMode(echo, INPUT); + + // provide pulse to trigger reading + digitalWrite(pulse, LOW); + delayMicroseconds(2); + digitalWrite(pulse, HIGH); + delayMicroseconds(5); + digitalWrite(pulse, LOW); + + // measure the echo time on the echo pin + int duration = pulseIn(echo, HIGH, 60000); + Serial.print(microsecondsToMm(duration)); + Serial.print("\n"); + return; + } + } + + // If we dont hit a return statement in the if/else tree + // we return a NACK due to it being an invalid command + Serial.print("NACK:Invalid command\n"); +} + +long microsecondsToMm(long microseconds) { + // The speed of sound is 340 m/s or 29 microseconds per centimeter. + // The ping travels out and back, so to find the distance we need half + // 10 x (us / 29 / 2) + return (5 * microseconds / 29); +} + +int getPinMode(uint8_t pin) { + // arduino functions to map a pin to a port/bit + uint8_t bit = digitalPinToBitMask(pin); + uint8_t port = digitalPinToPort(pin); + + // check if the direction is input or output + volatile uint8_t *reg = portModeRegister(port); + if (*reg & bit) { + return OUTPUT; + } + + // If pin mode is input check if pullup is enabled + volatile uint8_t *out = portOutputRegister(port); + return ((*out & bit) ? INPUT_PULLUP : INPUT); +}