diff --git a/source/main.cpp b/source/main.cpp index 9059259d87f4f859b7b95d30a8de88e3fc5ffe49..ded7c116f162cbd3c0d4ea0b0fe7a8957641d01a 100755 --- a/source/main.cpp +++ b/source/main.cpp @@ -26,6 +26,13 @@ #include "Transceiver.hpp" +// special config macros +#define MORSE_UNIT_MS 100 // One Morse "unit" is 100ms duration +// micro:bit pin number and pin name --important: both of these must correspond! +#define COMMS_PIN_NUMBER 1 +#define COMMS_PIN_NAME MICROBIT_PIN_P1 + + // please import all names from my namespace into local scope! using namespace com_saxbophone; @@ -33,7 +40,7 @@ using namespace com_saxbophone; MicroBit ubit; // a global MorseDecoder instance -MorseDecoder<> decoder(100); // set one "unit" to be 100ms long +MorseDecoder<> decoder(MORSE_UNIT_MS); /** @@ -53,12 +60,176 @@ Role get_my_role() { a_pressed = ubit.buttonA.isPressed(); b_pressed = ubit.buttonB.isPressed(); // NOTE: this is bitwise-XOR but given the boolean nature, it's acceptable - } while(not (a_pressed xor b_pressed)); // until exactly one button pressed + } while (not (a_pressed xor b_pressed)); // until exactly one button pressed // because of the XOR exit condition of loop, if a_pressed = 0, b_pressed = 1 return a_pressed ? Role::MASTER : Role::SLAVE; // A = MASTER, B = SLAVE } +class Pairer { +public: + /** + * @brief Default constructor + */ + Pairer() + : synchronisation_timestamp(0) // zero-initialise to prevent any issues + {} + + /** + * @brief Attempt to pair with another connected micro:bit + * @param my_role Whether we wish to be the Master or Slave for comms + * purposes + * @returns true if paired successfully + * @returns false if unable to pair successfully + * @note This is a blocking call! + */ + bool attempt_pair(Role my_role) { + if (my_role == Role::MASTER) { + // attempt pair as Master + return this->pair_as_master(); + } else if (my_role == Role::SLAVE) { + // TODO: Attempt pair as Slave + return false; + } else { + // invalid Role! + return false; + } + } + +private: + bool pair_as_master() { + int pin_state = ubit.io.pin[COMMS_PIN_NUMBER].getDigitalValue(); + // clarify if someone else is driving the pin HI or if pin is invalid + switch (pin_state) { + case 0: + break; // this is good, it means no-one else is driving the pin + case 1: + /* DELIBERATE FALLTHROUGH */ + case MICROBIT_NOT_SUPPORTED: + /* + * either someone else on the line thinks they're Master, or we've + * been configured with a pin that can't do Digital I/O. In either + * case, this is a failure condition. + */ + return false; + } + // successful execution continues here + // wait for the pin to stay LO for 500ms + unsigned long start_time = ubit.systemTime(); + unsigned long now_time = start_time; + do { + /* + * NOTE: this is a busy wait loop with no sleep() + * this is okay as we're deliberately allowing this function to + * block the entire system, it's documented as such and we also can + * only sleep to a 6ms precision, but we want to time as accurately + * as possible for the pairing process + */ + now_time = ubit.systemTime(); + pin_state = ubit.io.pin[COMMS_PIN_NUMBER].getDigitalValue(); + } while (pin_state == 0 and (now_time - start_time) < 500); + // check to make sure we didn't break out of the loop because of HI pin + if (pin_state != 0) return false; + // there's definitely no-one else transmitting on the line, now we transmit + start_time = ubit.systemTime(); + now_time = start_time; + ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(1); + // wait 500ms + do { + /* + * NOTE: as with the previous loop, this is a busy wait loop + * it's particularly important that this one is accurately timed, + * as we expect ourself and the Slave to synchronise clocks on the + * falling edge of this pulse we send + */ + now_time = ubit.systemTime(); + } while ((now_time - start_time) < 500); + // bring line LO + ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(0); + // now_time is the clock synchronisation timestamp + this->synchronisation_timestamp = now_time; + // now, wait up to 100ms for a reply from the Slave + this->busy_wait_on_pin(0, 100, pin_state, start_time, now_time); + // this time, check that the pin is HI --it must be HI, otherwise timeout + if (pin_state != 1) return false; + // now, Slave should keep the Line HI for at least 450ms + this->busy_wait_on_pin(1, 450, pin_state, start_time, now_time); + // if the pin is LO, then Slave didn't drive it long enough + if (pin_state == 0) return false; + // otherwise, pin is still HI --allow Slave up to 100ms to bring it LO + this->busy_wait_on_pin(1, 100, pin_state, start_time, end_time); + // if the pin is still HI, then Slave drove the pin for too long + if (pin_state == 1) return false; + // otherwise, all we need to do now is to wait for synchronisation + return this->await_synchronisation(); + } + + bool pair_as_slave(); + + /** + * @brief Performs a busy-wait loop, continuously checking the comms pin + * to make sure it is in a given state for a certain amount of time + * @details method returns once either the pin is no longer in the desired + * state, or if the given timeout has passed + * @param keep_state the desired state (HI = 1 or LO = 0) that the pin is to + * keep + * @param timeout the maximum amount of time that the method will detect the + * pin holding this state (and block) for + * @param pin_state[out] the last detected pin state will be stored here + * @param start_time[out] the system time that detection commenced (ms) will + * be stored here + * @param end_time[out] the system time that detection ended (ms) will be + * stored here + */ + void busy_wait_on_pin( + int keep_state, + unsigned long timeout + int& pin_state, + unsigned long& start_time, + unsigned long& end_time + ) { + // initialise time counters + start_time = ubit.systemTime(); + end_time = start_time; + // wait up to the specified timeout + do { + /* + * NOTE: this is a busy wait loop with no sleep() + * this is okay as we're deliberately allowing this function to + * block the entire system, it's documented as such and we also can + * only sleep to a 6ms precision, but we want to time as accurately + * as possible + */ + end_time = ubit.systemTime(); + pin_state = ubit.io.pin[COMMS_PIN_NUMBER].getDigitalValue(); + } while (pin_state == keep_state and (end_time - start_time) < timeout); + } + + /** + * @brief Blocks caller until 1250ms have elapsed since the stored + * synchronisation timestamp + * @returns true if the timestamp was in the future and the synchronisation + * point has been reached + * @returns false if the timestamp was not in the future (in this case, + return is immediate) + * @warn Don't call this if this->synchronisation_timestamp hasn't been set + * first! + */ + bool await_synchronisation() { + // calculate synchronisation time and check it's in the future + unsigned long sync_time = this->synchronisation_timestamp + 1250; + unsigned long now_time = ubit.systemTime(); + if (sync_time < now_time) return false; // can't do it, sync point in past + // busy wait loop to wait until synchronisation time is reached + do { + now_time = ubit.systemTime(); + } while (now_time < sync_time); + } + + unsigned long synchronisation_timestamp; +}; + + /* * a very simple class that tracks the button state as it changes and the * duration between changes @@ -203,7 +374,7 @@ int main() { Role my_role = get_my_role(); // this call blocks and asks the user // try to pair and if unsuccessful, go back to top of loop to restart - if (not transceiver.pair(my_role)) continue; + if (not attempt_pair(my_role)) continue; // otherwise, if execution continues here then we're paired // for now, we only act on button presses if we're the Master