diff --git a/.gitignore b/.gitignore
old mode 100755
new mode 100644
diff --git a/.yotta_ignore b/.yotta_ignore
old mode 100755
new mode 100644
diff --git a/module.json b/module.json
old mode 100755
new mode 100644
diff --git a/source/CircularQueueBuffer.hpp b/source/CircularQueueBuffer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c9c5eff8a97bb17607a65407cbb1df60eb876aea
--- /dev/null
+++ b/source/CircularQueueBuffer.hpp
@@ -0,0 +1,191 @@
+#ifndef COM_SAXBOPHONE_CIRCULAR_QUEUE_BUFFER_HPP
+#define COM_SAXBOPHONE_CIRCULAR_QUEUE_BUFFER_HPP
+
+/**
+ * @file
+ *
+ * @brief A templated class type implementing a simple circular queue buffer
+ *
+ * @details This is useful in many places in the project to implement simple
+ * buffers, the contents of which don't need shifting as items are removed from
+ * the head. This type automatically handles index-translation.
+ * @note As this is a templated type, this is a header-only compilation unit.
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <cstddef> // size_t
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    template<
+        typename T, /**< the type being stored */
+        size_t BUFFER_SIZE = 16u /**< default buffer size */
+    >
+    class CircularQueueBuffer {
+    public:
+        /**
+         * @brief Default Constructor
+         */
+        CircularQueueBuffer()
+            : buffer_items() // zero-initialise all members
+            , buffer_head(0)
+            , buffer_length(0)
+            {}
+
+        /**
+         * @brief Tries to add a new item to the end of the buffer
+         * @returns true if item was added successfully
+         * @returns false if the buffer is full and item could not be added
+         */
+        bool add(T item) {
+            // guard clause to prevent adding to a full buffer
+            if (this->is_full()) return false;
+            // if it's not full, then store the item
+            this->buffer_items[this->next_index()] = item;
+            // and update the buffer book-keeping
+            this->buffer_length++;
+            return true; // done successfully
+        }
+
+        /**
+         * @brief Overloaded [] operator to allow indexing into the buffer in
+         * an array-like fashion, for read-only purposes only
+         * @returns the item at the given index
+         * @warn Don't try and retrieve items with invalid indexes!
+         */
+        T operator[](size_t i) const {
+            return this->buffer_items[this->translate_index(i)];
+        }
+
+        /**
+         * @returns The current length of this buffer
+         * @note In this context, "length" refers to how many items are
+         * currently stored. To retrieve the total capacity of the buffer,
+         * see `capacity()`.
+         */
+        size_t length() const {
+            return this->buffer_length;
+        }
+
+        /**
+         * @returns the total capacity of this buffer
+         */
+        constexpr size_t capacity() const {
+            return BUFFER_SIZE;
+        }
+
+        /**
+         * @returns whether this buffer is empty or not
+         */
+        bool is_empty() const {
+            return this->buffer_length == 0;
+        }
+
+        /**
+         * @returns whether this buffer is full or not
+         */
+        bool is_full() const {
+            return this->buffer_length == BUFFER_SIZE;
+        }
+
+        /**
+         * @brief Empties the entire contents of the buffer
+         * @note If you know you want to empty the buffer, this is more
+         * efficient than calling shrink_from_front()
+         */
+        void clear() {
+            this->buffer_head = 0;
+            this->buffer_length = 0;
+        }
+
+        /**
+         * @brief Shrinks the buffer by removing up to `count` number of items
+         * from the front of it, moving the head of the buffer to the position
+         * after the last removed item and setting buffer length accordingly
+         * @returns true if buffer was successfully shrunk
+         * @returns false if buffer could not be shrunk by the given number of
+         * elements, because it has fewer elements than the requested amount
+         */
+        bool shrink_from_front(size_t count) {
+            // check count is not greater than buffer size before continuing
+            if (count > this->buffer_length) {
+                return false;
+            }
+            // optimisation: if full shrink is requested, just clear it
+            if (count == this->buffer_length) {
+                this->clear();
+                return true; // this can't fail
+            }
+            /*
+             * otherwise, continue and shrink the buffer
+             * NOTE: shrinking just requires changing head and length
+             * --no objects are destroyed
+             */
+            // the head of the buffer is moved forwards, respecting wrap-around
+            this->buffer_head = (this->buffer_head + count) % BUFFER_SIZE;
+            // the length is simply reduced by the number of elements
+            this->buffer_length -= count;
+            return true; // all done
+        }
+    private:
+        /**
+         * @returns the index of the first empty location within the transition
+         * buffer
+         */
+        size_t next_index() const {
+            // item at index buffer_length is the first item outside the buffer
+            return this->translate_index(this->buffer_length);
+        }
+
+        /**
+         * @returns the raw index of the given 0-index into the circular buffer
+         * @details Because the buffer is circular and isn't shifted around much,
+         * item at index 0 in the buffer as we visualise it might be at a
+         * different location than index 0 in memory. This method provides this
+         * translation.
+         * @warn Don't use this method with indexes at or above buffer_length
+         * unless you know what you're doing
+         * @warn Never use this method with indexes at or above BUFFER_SIZE
+         */
+        size_t translate_index(size_t i) const {
+            /*
+             * since this is a circular buffer, the tail of it will wrap around
+             * the end of the array in which it is stored eventually, so we need
+             * to account for that with some modulo arithmetic
+             */
+            return (this->buffer_head + i) % BUFFER_SIZE;
+        }
+
+        /**
+         * @brief Circular-buffer style queue used for storing items of type T.
+         * @details The size of this can be configured by changing the optional
+         * template non-type paramter BUFFER_SIZE to a different value. The size
+         * ought to be kept reasonably small to prevent excessive RAM usage on
+         * the RAM-constrained micro:bit, however a buffer size that is too
+         * small may cause premature capacity exhaustion in certain use cases.
+         */
+        T buffer_items[BUFFER_SIZE];
+        /**
+         * @brief the current index of the first item in buffer
+         */
+        size_t buffer_head;
+        /**
+         * @brief the current number of items stored in the buffer
+         */
+        size_t buffer_length;
+    };
+};
+
+#endif // include guard
diff --git a/source/ManchesterDecoder.cpp b/source/ManchesterDecoder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..50a0bea4311ef981671aa29d2afa6f3b5d37e39a
--- /dev/null
+++ b/source/ManchesterDecoder.cpp
@@ -0,0 +1,64 @@
+/**
+ * @file
+ *
+ * @brief Implementation of class ManchesterDecoder
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <cstdint> // uint8_t
+
+#include "ManchesterDecoder.hpp"
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    ManchesterDecoder::ManchesterDecoder()
+        : symbol_index(0) // the first symbol to read is definitely number 0
+        {}
+
+    bool ManchesterDecoder::input(bool state) {
+        // guard condition --if the buffer is full, do not accept further input
+        if (this->buffer.is_full()) return false;
+        // otherwise, the buffer has further space to cache
+        // every second received symbol is stored into the single byte buffer
+        if (this->symbol_index % 2) {
+            // {0..15} / 2 -> {0..7}        NOTE: big-endian
+            this->current_byte |= (state << (7 - (this->symbol_index / 2)));
+        }
+        // if this is the last symbol, then store byte in buffer and clear byte
+        if (this->symbol_index == 15) {
+            this->buffer.add(this->current_byte);
+            this->current_byte = 0x00u;
+        }
+        // always increment symbol index, wrap around at 16
+        this->symbol_index = (this->symbol_index + 1) % 16;
+        return true;
+    }
+
+    bool ManchesterDecoder::get_next_byte(uint8_t& destination) {
+        // guard condition --check if buffer has content
+        if (this->buffer.is_empty()) return false;
+        // read out the first item in buffer and remove it
+        destination = this->buffer[0];
+        this->buffer.shrink_from_front(1);
+        return true;
+    }
+
+    void ManchesterDecoder::clear() {
+        this->buffer.clear();
+        // also reset state variables
+        this->symbol_index = 0;
+        this->current_byte = 0x00u;
+    }
+};
diff --git a/source/ManchesterDecoder.hpp b/source/ManchesterDecoder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..55e770e428902e3bf8161a35b966f244e0705ea5
--- /dev/null
+++ b/source/ManchesterDecoder.hpp
@@ -0,0 +1,90 @@
+#ifndef COM_SAXBOPHONE_MANCHESTER_DECODER_HPP
+#define COM_SAXBOPHONE_MANCHESTER_DECODER_HPP
+
+/**
+ * @file
+ *
+ * @brief A simple class that performs Manchester Decoding of input line states.
+ *
+ * @details HI/LOW states are sent into the decoder with the input() method and
+ * decoded bytes are returned with the get_next_byte() method.
+ * Bytes are decoded in big-endian order, from a Manchester-encoded signal
+ * that conforms to that used in IEEE 802.3, that is, the clock cycles HI-LOW
+ * and output is Clock XOR Data. Thus, a HI-LOW transition indicates binary zero,
+ * whereas a LOW-HI transition indicates binary one.
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <cstdint> // uint8_t
+
+#include "CircularQueueBuffer.hpp"
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    class ManchesterDecoder {
+    public:
+        /**
+         * @brief Default constructor
+         */
+        ManchesterDecoder();
+
+        /**
+         * @brief Read in another state from the line to be decoded.
+         * @returns true if the state has been accepted
+         * @returns false if the state has not been accepted (buffer full)
+         */
+        bool input(bool state);
+
+        /**
+         * @brief Decodes the next byte from input (if available) and if there
+         * is a byte available, stores it in `destination`.
+         * @param destination[out] the byte to write the next decoded byte to
+         * @returns true if there was a byte available and it was stored
+         * @returns false if no bytes are available yet (destination is ignored)
+         */
+        bool get_next_byte(uint8_t& destination);
+
+        /**
+         * @brief Clears the input buffer, in case you want to do that.
+         */
+        void clear();
+    private:
+        /**
+         * @brief Counter tracking how many "symbols" (i.e. boolean input states)
+         * we have seen so far
+         * @details This wraps around at 16. 8 bits is represented as 16 symbols
+         * because of the integrated clock signal --as this implementation is
+         * IEEE 802.3, every second symbol holds the value of a decoded data bit
+         */
+        uint8_t symbol_index;
+
+        /**
+         * @brief Internal circular-buffer to cache decoded output bytes
+         * @detail Once the current_byte is full, it is pushed into this buffer,
+         * ready to be retrieved with get_next_byte()
+         */
+        CircularQueueBuffer<uint8_t> buffer;
+
+        /**
+         * @brief The current byte we are populating from decoded input bits
+         * @details This is used as a temporary cache to store the partially
+         * constructed input byte. Once the byte is full (all bits stored), it
+         * is added to the end of the buffer.
+         */
+        uint8_t current_byte;
+    };
+};
+
+#endif // include guard
diff --git a/source/ManchesterEncoder.cpp b/source/ManchesterEncoder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1e596aca7851ffa39b9f709fc65f933c48db3a6d
--- /dev/null
+++ b/source/ManchesterEncoder.cpp
@@ -0,0 +1,61 @@
+/**
+ * @file
+ *
+ * @brief Implementation of class ManchesterEncoder
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <cstdint> // uint8_t
+
+#include "ManchesterEncoder.hpp"
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    ManchesterEncoder::ManchesterEncoder()
+        : clock(true) // our implementation has the clock signal starting HI
+        , bit_index(0) // we definitely want to start reading bit 0
+        {}
+
+    bool ManchesterEncoder::input(uint8_t byte) {
+        return this->buffer.add(byte); // buffer will return false if it's full
+    }
+
+    bool ManchesterEncoder::get_next_state(int& destination) {
+        // guard condition --check if buffer has content
+        if (this->buffer.is_empty()) return false;
+        // Manchester Code = Clock XOR Data
+        int symbol = this->clock xor this->extract_bit(
+            this->buffer[0],
+            this->bit_index
+        );
+        destination = symbol;
+        // if we are at the end of a byte
+        if (this->clock == false and this->bit_index == 7) {
+            // shrink the buffer from the front by 1 to advance the buffer
+            this->buffer.shrink_from_front(1);
+        }
+        // update bit index whenever clock is LOW (end of duration of 1 data bit)
+        if (not this->clock) this->bit_index = (this->bit_index + 1u) % 8u;
+        // update clock every time
+        this->clock = not this->clock;
+        return true;
+    }
+
+    bool ManchesterEncoder::extract_bit(uint8_t byte, uint8_t bit_index) const {
+        // NOTE: this is a big-endian extraction
+        uint8_t mask = 0x80u >> bit_index;
+        return (byte & mask); // no need to shift down, anything non-zero is true
+    }
+};
diff --git a/source/ManchesterEncoder.hpp b/source/ManchesterEncoder.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c73543d23c46e157aae2b27221376b3041dfc8c2
--- /dev/null
+++ b/source/ManchesterEncoder.hpp
@@ -0,0 +1,97 @@
+#ifndef COM_SAXBOPHONE_MANCHESTER_ENCODER_HPP
+#define COM_SAXBOPHONE_MANCHESTER_ENCODER_HPP
+
+/**
+ * @file
+ *
+ * @brief A simple class that performs Manchester Encoding of input bytes
+ *
+ * @details Bytes are sent into the encoder with the input() method and line
+ * states to set (HI/LO) are returned with the get_next_state() method.
+ * Bytes are encoded in big-endian order and the Manchester Encoding version
+ * conforms to that used in IEEE 802.3, that is, the clock cycles HI-LOW and
+ * output is Clock XOR Data. Thus, a HI-LOW transition indicates binary zero,
+ * whereas a LOW-HI transition indicates binary one.
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <cstdint> // uint8_t
+
+#include "CircularQueueBuffer.hpp"
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    class ManchesterEncoder {
+    public:
+        /**
+         * @brief Default constructor
+         */
+        ManchesterEncoder();
+
+        /**
+         * @brief Read in another byte of input to be encoded by the encoder
+         * @returns true if the byte has been accepted
+         * @returns false if the byte has not been accepted (buffer full)
+         */
+        bool input(uint8_t byte);
+
+        /**
+         * @brief retrieves the next state that the encoder wants the output
+         * line to be set to, where 0 = LOW and 1 = HIGH, stores it in
+         * `destination`.
+         * @param destination[out] the int to write the next state to
+         * @returns true if there was a state available and it was stored
+         * @returns false if no states are available yet (destination is ignored)
+         */
+        bool get_next_state(int& destination);
+    private:
+        /**
+         * @brief Extracts the bit at given index from the given byte
+         * @note this uses a big-endian bit addressing system
+         */
+        bool extract_bit(uint8_t byte, uint8_t bit_index) const;
+
+        /**
+         * @brief Clock flag used to track this ManchesterEncoder's clock signal
+         * @details The clock signal of a Manchester Encoder alternates at twice
+         * the rate of data. In our implementation, our clock alternates HI-LO
+         * in that order.
+         */
+        bool clock;
+
+        /**
+         * @brief Which bit of the byte of data we are currently on
+         * @details This cycles in the range {0..7} and tells us which bit to
+         * read out of the byte at the head of the buffer next. When it
+         * overflows, it's time to advance the buffer and read the next byte
+         */
+        uint8_t bit_index;
+
+        /**
+         * @brief Internal circular-buffer to cache input bytes
+         */
+        CircularQueueBuffer<uint8_t> buffer;
+
+        /**
+         * @brief The current byte we have started sending
+         * @details Bytes are extracted from the buffer into here. If the buffer
+         * is ever exhausted, 0x00 is placed here to give the encoder something
+         * to encode.
+         */
+        uint8_t current_byte;
+    };
+};
+
+#endif // include guard
diff --git a/source/MorseDecoder.hpp b/source/MorseDecoder.hpp
index 36dd9d8e27dd049de8bbcff8d6c768aa38bf8f4e..c1cc963d5193710a07dc15dffe7ef5b0006d9059 100644
--- a/source/MorseDecoder.hpp
+++ b/source/MorseDecoder.hpp
@@ -28,18 +28,21 @@
 
 #include <cstddef> // size_t
 
+#include "CircularQueueBuffer.hpp"
 #include "MorseSymbol.hpp"
 #include "MorseTree.hpp"
+#include "Pulse.hpp"
 
 
 // SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
 namespace com_saxbophone {
-    template<size_t BUFFER_SIZE = 16u> /**< the size to use for the transition buffer */
     class MorseDecoder {
     public:
         /**
          * @brief Constructor
          * @param unit_duration the duration of one Morse Code 'unit' in ms
+         * @todo Consider converting to constexpr to allow const derivation of
+         * unit durations at compile-time.
          */
         MorseDecoder(unsigned long unit_duration)
             // pre-compute the duration of symbols in ms
@@ -84,18 +87,14 @@ namespace com_saxbophone {
              * false every time end of input is detected.
              */
             // if execution reaches this point, we know we need to store
-            // detect if buffer is about to overflow and return failure if it is
-            if (this->buffer_length == BUFFER_SIZE) {
+            // try and store a new Pulse constructed from these params
+            Pulse p = { held, duration, };
+            if (not this->buffer.add(p)) {
                 // oh no, a buffer *would* have overflown, but we won't let it!
                 return false;
             }
-            // if execution reaches this point, we know we can store
-            Pulse& destination = this->transitions[this->next_buffer_index()];
-            destination.held = held;
-            destination.duration = duration;
-            // update buffer book-keeping, as well as last-seen
-            this->buffer_length++;
-            this->last_seen = held;
+            // if execution reached here, we know store was successful
+            this->last_seen = held; // update last seen
             // TODO: do we have to do anything else at this point? Probably not
             return true;
         }
@@ -131,7 +130,7 @@ namespace com_saxbophone {
              * the minimum number of transitions we need for a symbol is 2
              * (E and T consist of one pulse, so that'll be one on and one off)
              */
-            if (this->buffer_length < 2) {
+            if (this->buffer.length() < 2) {
                 return false; // not enough content in the buffer yet to decode
             }
             // use the MorseTree to try and decode some morse
@@ -153,7 +152,7 @@ namespace com_saxbophone {
                 return false;
             }
             // in all cases, we now need to remove the parsed pulses from buffer
-            this->shrink_buffer_from_front(pulses_read);
+            this->buffer.shrink_from_front(pulses_read);
             // now read the tree's cursor, then we can parse it
             char symbol = decoder_tree.read_cursor();
             switch (symbol) {
@@ -204,42 +203,6 @@ namespace com_saxbophone {
         };
 
     private:
-        /**
-         * @brief Internal type used to track Morse input 'transitions'/pulses
-         */
-        struct Pulse {
-            bool held; /**< whether this Pulse is a button held down or not */
-            unsigned long duration; /**< the duration of this Pulse */
-        };
-
-        /**
-         * @returns the index of the first empty location within the transition
-         * buffer
-         */
-        size_t next_buffer_index() const {
-            // item at index buffer_length is the first item outside the buffer
-            return this->buffer_index(this->buffer_length);
-        }
-
-        /**
-         * @returns the raw index of the given 0-index into the circular buffer
-         * @details Because the buffer is circular and isn't shifted around much,
-         * item at index 0 in the buffer as we visualise it might be at a
-         * different location than index 0 in memory. This method provides this
-         * translation.
-         * @warn Don't use this method with indexes at or above buffer_length
-         * unless you know what you're doing
-         * @warn Never use this method with indexes at or above BUFFER_SIZE
-         */
-        size_t buffer_index(size_t i) const {
-            /*
-             * since this is a circular buffer, the tail of it will wrap around
-             * the end of the array in which it is stored eventually, so we need
-             * to account for that with some modulo arithmetic
-             */
-            return (this->buffer_head + i) % BUFFER_SIZE;
-        }
-
         /**
          * @brief Attempts to read transitions from the buffer, converting them
          * to Pulses and sending them into a MorseTree, which is returned
@@ -253,21 +216,14 @@ namespace com_saxbophone {
             size_t* pulses_read,
             bool* found_end
         ) const {
-            // TODO: add proper implementation:
-            // TODO: parse transitions and convert to Pulses
-            // TODO: allow dots and dashes to be sent to the tree
-            // TODO: check if a LETTER SPACE or WORD SPACE were found
-            // TODO: return the tree
-
             bool stop = false;
-
             // instantiate a MorseTree decoder we will use to try and decode
             MorseTree decoder_tree;
             // keep reading until we either exhaust the buffer or reach a space
             size_t i; // we'll re-use this after looping to count how pulses
-            for (i = 0; not stop and i < this->buffer_length; i++) {
+            for (i = 0; not stop and i < this->buffer.length(); i++) {
                 // retrieve the Pulse from buffer
-                Pulse input = this->transitions[this->buffer_index(i)];
+                Pulse input = this->buffer[i];
                 // convert it to a Morse symbol
                 MorseSymbol symbol = this->convert_transition_to_morse(input);
                 // decide what to do with the symbol
@@ -303,7 +259,7 @@ namespace com_saxbophone {
          */
         MorseSymbol convert_transition_to_morse(Pulse transition) const {
             // first of all, distinguish between counting marks or spaces
-            if (transition.held) { // it's a dot or a dash ("mark")
+            if (transition.state) { // it's a dot or a dash ("mark")
                 /*
                  * across the alphanumeric range of International Morse, Dots
                  * are slightly more common than Dashes (68 Dots vs 63 Dashes
@@ -338,35 +294,6 @@ namespace com_saxbophone {
             return MorseSymbol::UNKNOWN;
         }
 
-        /**
-         * @brief Shrinks this decoder's internal transition buffer by removing
-         * up to `count` number of items from the front of it, moving the head
-         * of the buffer to the position after the last removed item and setting
-         * buffer length accordingly
-         * @todo Consider adding an additional nicety that resets the buffer
-         * head position to zero if buffer is emptied (not sure if there is any
-         * benefit to doing this)
-         * @returns true if buffer was successfully shrunk
-         * @returns false if buffer could not be shrunk by the given number of
-         * elements, because it has fewer elements than the requested amount
-         */
-        bool shrink_buffer_from_front(size_t count) {
-            // check count is not greater than buffer size before continuing
-            if (count > this->buffer_length) {
-                return false;
-            }
-            /*
-             * otherwise, continue and shrink the buffer
-             * NOTE: shrinking just requires changing head and length
-             * --no objects are destroyed
-             */
-            // the head of the buffer is moved forwards, respecting wrap-around
-            this->buffer_head = (this->buffer_head + count) % BUFFER_SIZE;
-            // the length is simply reduced by the number of elements
-            this->buffer_length -= count;
-            return true; // all done
-        }
-
         /**
          * @brief The number of units long that a Morse Code "DOT" is
          */
@@ -418,20 +345,12 @@ namespace com_saxbophone {
          * @brief Circular-buffer style queue used to store transitions as
          * passed into this decoder when transition() is called.
          * @details The size of this can be configured by changing the optional
-         * template non-type paramter BUFFER_SIZE to a different value. The size
+         * second template non-type paramter to a different value. The size
          * ought to be kept reasonably small to prevent excessive RAM usage on
          * the RAM-constrained micro:bit, however a buffer size that is too
          * small may leave the MorseDecoder unable to function!
          */
-        Pulse transitions[BUFFER_SIZE];
-        /**
-         * @brief the current index of the first item in the transitions buffer
-         */
-        size_t buffer_head;
-        /**
-         * @brief the current number of items stored in the buffer
-         */
-        size_t buffer_length;
+        CircularQueueBuffer<Pulse> buffer;
 
         /**
          * @brief the duration of one Morse Code "unit" in ms
diff --git a/source/MorseSymbol.hpp b/source/MorseSymbol.hpp
index c1c6fdab9eb46cd40cc1919f080b2ad916e98b49..12f8786373e410443f5f286fba288098c3a7c79c 100644
--- a/source/MorseSymbol.hpp
+++ b/source/MorseSymbol.hpp
@@ -6,12 +6,6 @@
  *
  * @brief A simple data type used to represent the symbols of Morse Code
  *
- * @details TODO ADD DESCRIPTION HERE
- *
- * @todo Move the implementation code out of this header and into a
- * corresponding .cpp file. I can't do this right now as I currently don't know
- * how to make Yotta build multiple objects...
- *
  * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
  * @date 2020
  *
@@ -25,7 +19,6 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
-
 // SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
 namespace com_saxbophone {
     /**
diff --git a/source/Pulse.hpp b/source/Pulse.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ed65233a1cf48340f73a12681e45dd19b695a6ee
--- /dev/null
+++ b/source/Pulse.hpp
@@ -0,0 +1,38 @@
+#ifndef COM_SAXBOPHONE_PULSE_HPP
+#define COM_SAXBOPHONE_PULSE_HPP
+
+/**
+ * @file
+ *
+ * @brief POD type used for representing morse code and digital signals.
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    /**
+     * @brief represents input pulses of Morse Code or digital signals.
+     */
+    struct Pulse {
+        /**
+         * @brief Whether this pulse is HI/button held down or LO/button released
+         */
+        bool state;
+        unsigned long duration; /**< the duration of this Pulse */
+    };
+
+};
+
+#endif // include guard
diff --git a/source/Role.hpp b/source/Role.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..36ec489913dc930bb7d1bc26488736ada1899ea9
--- /dev/null
+++ b/source/Role.hpp
@@ -0,0 +1,34 @@
+#ifndef COM_SAXBOPHONE_ROLE_HPP
+#define COM_SAXBOPHONE_ROLE_HPP
+
+/**
+ * @file
+ *
+ * @brief Communication role of the participating micro:bits
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    /**
+     * @brief Communication Role to be used by a micro:bit
+     */
+    enum class Role {
+        UNKNOWN, /**< unknown Role (sentinel for zero-initialised data) */
+        SLAVE, /**< Slave Role */
+        MASTER, /**< Master Role */
+    };
+};
+
+#endif // include guard
diff --git a/source/Transceiver.hpp b/source/Transceiver.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b71da6d57384e2a08d7b465abdfaf751cfc91bc8
--- /dev/null
+++ b/source/Transceiver.hpp
@@ -0,0 +1,70 @@
+#ifndef COM_SAXBOPHONE_TRANSCEIVER_HPP
+#define COM_SAXBOPHONE_TRANSCEIVER_HPP
+
+/**
+ * @file
+ *
+ * @brief A class providing a very basic trasceiver, that only knows how to pair
+ * two micro:bits together and how to encode bytes sent to it into Manchester
+ * Code.
+ *
+ * @details TODO ADD DESCRIPTION HERE
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+#include <cstdint>
+
+#include "Role.hpp"
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    /**
+     * @brief A basic class responsible only for low-level comms
+     */
+    class Transceiver {
+    public:
+        /**
+         * @brief Used to allow synchronising both of a paired Master and Slave
+         * @details Blocks the caller until a point in time a fixed duration
+         * after the pairing synchronisation epoch is reached, if such point is
+         * in the future. Otherwise, returns immediately.
+         * @returns true if called between successful pairing and the
+         * synchronisation epoch.
+         * @returns false if called whilst not paired, or if the synchronisation
+         * epoch is in the past
+         */
+        bool barrier();
+
+        /**
+         * @brief Attempts to send the given byte to the connected peer
+         * @details This method is non-blocking
+         * @returns true if this was done successfully
+         * @returns false if this could not be done
+         * @warn Bytes can only be sent once the Transceiver has been paired!
+         * @warn Currently only the Master can send
+         * @note This is a very low-level API with no receipt-confirmation, so
+         * this method cannot tell you if the sent byte was received
+         * successfully by the remote peer!
+         */
+        bool send(uint8_t byte);
+
+        /**
+         * @brief Closes down the connection and stops any currently active
+         * transmission.
+         */
+        void close();
+    };
+};
+
+#endif // include guard
diff --git a/source/TransceiverConfig.hpp b/source/TransceiverConfig.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f2bafa2b9c9ea28f240bd2539fdbd36dd5a6c098
--- /dev/null
+++ b/source/TransceiverConfig.hpp
@@ -0,0 +1,70 @@
+#ifndef COM_SAXBOPHONE_TRANSCEIVER_CONFIG_HPP
+#define COM_SAXBOPHONE_TRANSCEIVER_CONFIG_HPP
+
+/**
+ * @file
+ *
+ * @brief POD type used for storing transceiver protocol config.
+ * @note All config parameters are static, therefore this is the global config.
+ *
+ * @author Joshua Saxby <Joshua2.Saxby@live.uwe.ac.uk>
+ * @date 2020
+ *
+ * Student Number 18018052
+ *
+ * @copyright Copyright (C) Joshua Saxby 2020
+ *
+ * @copyright
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+
+// SAXBOPHONE.COM is my domain, so I'm using the reverse form as my namespace
+namespace com_saxbophone {
+    /**
+     * @brief represents input pulses of Morse Code or digital signals.
+     */
+    struct TransceiverConfig {
+        /**
+         * @brief Pulses longer than this duration are ignored for decoding.
+         * @details This is set to 200000µs (200ms)
+         */
+        const static unsigned long MAXIMUM_PULSE_DURATION = 120000;
+
+        /**
+         * @brief STOP/START bits are useful for demarcating when a transmission
+         * begins and ends.
+         * @details This is set to 600µs (6ms)
+         */
+        const static unsigned long STOP_START_BIT_DURATION = 6000;
+        const static unsigned long STOP_START_BIT_DURATION_MS = 6;
+
+        /**
+         * @brief This is the duration of a standard raw bit on the signal line.
+         * @details This is set to 24000µs (24ms)
+         */
+        const static unsigned long SIGNAL_BIT_DURATION = 42000;
+        const static unsigned long SIGNAL_BIT_DURATION_MS = 42;
+
+        /**
+         * @brief This is the margin of error we allow for when deciding what
+         * type a pulse is based on duration.
+         * @note 1.0 = 100% and 0.0 = 0%
+         * @details We currently allow a ±30% timing margin
+         */
+        constexpr const static double DURATION_MARGIN = 0.30;
+
+        /**
+         * @brief This is the total amount of time allocated for one transmission
+         * @details This includes START/STOP bits, message bits and a bit of
+         * silence time for good measure. This is set to 900ms.
+         * @note This is the only duration in this struct to use ms for units
+         */
+        const static unsigned long TRANSMISSION_WINDOW = 900;
+    };
+
+};
+
+#endif // include guard
diff --git a/source/main.cpp b/source/main.cpp
old mode 100755
new mode 100644
index e5b7a9ef66eac61ef0ff0db49b54624a0ee92f40..14451b98a956a9c7530fa337f8c96d21f419406a
--- a/source/main.cpp
+++ b/source/main.cpp
@@ -21,16 +21,311 @@
 
 #include "MicroBit.h"
 
+#include "ManchesterDecoder.hpp"
+#include "ManchesterEncoder.hpp"
 #include "MorseDecoder.hpp"
+#include "Pulse.hpp"
+#include "Role.hpp"
+#include "Transceiver.hpp"
+#include "TransceiverConfig.hpp"
 
 
-// our single global MicroBit instance
-MicroBit ubit;
+// 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
+#define COMMS_PIN_EVENT_ID MICROBIT_ID_IO_P1
+
 
 // please import all names from my namespace into local scope!
 using namespace com_saxbophone;
-// a global MorseDecoder instance
-MorseDecoder<> decoder(100); // set one "unit" to be 100ms long
+
+// our single global MicroBit instance
+MicroBit ubit;
+
+
+/**
+ * @brief Simple function for just working out whether this micro:bit is going
+ * to be the Master or Slave for communications purposes
+ * @warn This method blocks until exactly one of button A or B is pressed!
+ */
+Role get_my_role() {
+    // ubit.serial.send("get_my_role()");
+    // scroll some stuff on the screen to prompt the user to choose
+    ubit.display.scroll("<M"); // point left to Button A to choose Master
+    ubit.sleep(100); // a brief pause
+    ubit.display.scroll("S>"); // point right to Button B to choose Slave
+    bool a_pressed = false;
+    bool b_pressed = false;
+    do {
+        ubit.sleep(10); // give the CPU a little bit of a break
+        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
+    // 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 0 if paired successfully
+     * @returns error code number if unable to pair successfully
+     * @note This is a blocking call!
+     */
+    int 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) {
+            // attempt pair as Slave
+            return this->pair_as_slave();
+        } else {
+            // invalid Role!
+            return -1;
+        }
+    }
+
+    /**
+     * @brief Returns to the caller once the amount of time since synchronisation
+     * reaches a multiple of the transmission window.
+     * @details This is intended to be used to keep both Master and Slave
+     * synchronised. Assuming their clocks were synchronised correctly, both can
+     * call this and the calls should return at near enough the same point in time.
+     */
+    void barrier() {
+        // TODO: consider allowing for a ±10% margin of error
+        unsigned long start_time = ubit.systemTime(); // what time is it now?
+        // this is how much time we have left to next sync point
+        unsigned long time_left = TransceiverConfig::TRANSMISSION_WINDOW - (
+            (start_time - this->synchronisation_timestamp) %
+            TransceiverConfig::TRANSMISSION_WINDOW
+        );
+        // calculate the time of the next sync point
+        unsigned long sync_point = start_time + time_left;
+        /*
+         * systemTime() can only sleep to 6ms resolution, so sleep to the nearest
+         * multiple of 6ms, rounded down
+         */
+        ubit.sleep((time_left / 6) * 6);
+        // busy wait loop sleeps for remainder of time
+        unsigned long time_now;
+        do {
+            time_now = ubit.systemTime();
+        } while (time_now < sync_point);
+    }
+
+private:
+    int 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.
+             */
+            // ubit.serial.send("MICROBIT_NOT_SUPPORTED\r\n");
+            return 2;
+        }
+        // successful execution continues here
+        // wait for the pin to stay LO for 500ms
+        unsigned long start_time, end_time;
+        this->busy_wait_on_pin(0, 500, pin_state, start_time, end_time);
+        // check to make sure we didn't break out of the loop because of HI pin
+        if (pin_state != 0) {
+            // ubit.serial.send("Other Master on Line\r\n");
+            return 4;
+        }
+        // there's definitely no-one else transmitting on the line, now we transmit
+        this->busy_wait_drive_pin(500, start_time, end_time);
+        // end_time is the clock synchronisation timestamp
+        this->synchronisation_timestamp = end_time;
+        // now, wait up to 200ms for a reply from the Slave
+        this->busy_wait_on_pin(0, 200, pin_state, start_time, end_time);
+        // this time, check that the pin is HI --it must be HI, otherwise timeout
+        if (pin_state != 1) {
+            // ubit.serial.send("Timeout waiting for Slave\r\n");
+            return 6;
+        }
+        // ubit.serial.printf("Slave Latency: %i\r\n", end_time - start_time);
+        // now, Slave should keep the Line HI for at least 400ms
+        this->busy_wait_on_pin(1, 400, pin_state, start_time, end_time);
+        // if the pin is LO, then Slave didn't drive it long enough
+        // ubit.serial.printf("Slave Reply: %i\r\n", end_time - start_time);
+        if (pin_state == 0) {
+            // ubit.serial.send("Slave Reply too SHORT\r\n");
+            return 8;
+        }
+        // otherwise, pin is still HI --allow Slave up to 200ms to bring it LO
+        this->busy_wait_on_pin(1, 200, pin_state, start_time, end_time);
+        // ubit.serial.printf("Slave Reply: %i\r\n", end_time - start_time);
+        // if the pin is still HI, then Slave drove the pin for too long
+        if (pin_state == 1) {
+            // ubit.serial.send("Slave Reply too LONG\r\n");
+            return 10;
+        }
+        // otherwise, all we need to do now is to wait for synchronisation
+        return this->await_synchronisation();
+    }
+
+    int pair_as_slave() {
+        // check the pin can read digital values
+        int pin_state = ubit.io.pin[COMMS_PIN_NUMBER].getDigitalValue();
+        if (pin_state == MICROBIT_NOT_SUPPORTED) {
+            // ubit.serial.send("MICROBIT_NOT_SUPPORTED\r\n");
+            return 1;
+        } // oh no, it can't
+        // otherwise, it can read digital values
+        // now, wait up to 10000ms for Master to drive the line HI
+        unsigned long start_time, end_time;
+        this->busy_wait_on_pin(0, 10000, pin_state, start_time, end_time);
+        // clarify the loop ended because pin is now indeed HI
+        if (pin_state != 1) {
+            // ubit.serial.send("Timeout waiting for Master\r\n");
+            return 3;
+        } // we timed out
+        // otherwise, line is HI, wait for it to stay that way at least 400ms
+        this->busy_wait_on_pin(1, 400, pin_state, start_time, end_time);
+        // ubit.serial.printf("Master Probe: %i\r\n", end_time - start_time);
+        // clarify the loop ended because timeout occurred (pin should be HI)
+        if (pin_state != 1) {
+            // ubit.serial.send("Master Probe too SHORT\r\n");
+            return 5;
+        } // pin went LO, that's not correct
+        // wait up to 200 more ms for line to go LO
+        this->busy_wait_on_pin(1, 200, pin_state, start_time, end_time);
+        // ubit.serial.printf("Master Probe: %i\r\n", end_time - start_time);
+        // if line is still HI, then it's overrun
+        if (pin_state == 1) {
+            // ubit.serial.send("Master Probe too LONG\r\n");
+            return 7;
+        }
+        // otherwise, it's now our turn to bring the line HI
+        // the most recent end_time is also the sync point, store that first
+        this->synchronisation_timestamp = end_time;
+        // drive line HI
+        this->busy_wait_drive_pin(500, start_time, end_time);
+        // now we just need to await synchronisation
+        return this->await_synchronisation();
+    }
+
+    /**
+     * @brief Drives the comms pin HI and performs a busy-wait loop for a given
+     * duration, before driving the pin LO again.
+     * @param duration length of time in ms to drive the pin HI for
+     * @param start_time[out] system timestamp (ms) that the pin went HI
+     * @param end_time[out] system timestamp (ms) that the pin went LO
+     */
+    void busy_wait_drive_pin(
+        unsigned long duration,
+        unsigned long& start_time,
+        unsigned long& end_time
+    ) {
+        start_time = ubit.systemTime();
+        end_time = start_time;
+        ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(1);
+        // wait for specified duration
+        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();
+        } while ((end_time - start_time) < duration);
+        // bring line LO
+        ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(0);
+    }
+
+    /**
+     * @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!
+     */
+    int await_synchronisation() {
+        // calculate synchronisation time and check it's in the future
+        unsigned long sync_time = this->synchronisation_timestamp + 2500;
+        // ubit.serial.printf("Sync Stamp: %i\r\n", sync_time);
+        unsigned long now_time = ubit.systemTime();
+        if (sync_time < now_time) return 11; // 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);
+        return 0; // at this point, now both Master and Slave are synchronised
+    }
+
+    unsigned long synchronisation_timestamp;
+};
+
+
+// a global MorseDecoder instance --ButtonTracker needs access to it so declare
+MorseDecoder morse_decoder(MORSE_UNIT_MS);
 
 
 /*
@@ -43,7 +338,12 @@ public:
         : current_state(false) // button always starts UP (not held down)
         , last_update(0) // initialise this to zero --no events yet
         , started_yet(false) // wait until our first UP/DOWN event
-        {
+        {}
+
+    /**
+     * @brief Initialises this ButtonTracker, including attaching event-handler
+     */
+    void init() {
         // set up event handler(s)
         ubit.messageBus.listen(
             MICROBIT_ID_BUTTON_A,
@@ -53,6 +353,28 @@ public:
         );
     }
 
+    /**
+     * @brief Tears down this ButtonTracker, including removing event-handler
+     */
+    void stop() {
+        // set up event handler(s)
+        ubit.messageBus.ignore(
+            MICROBIT_ID_BUTTON_A,
+            MICROBIT_EVT_ANY,
+            this,
+            &ButtonTracker::on_button_a_change
+        );
+    }
+
+    /**
+     * @returns true if input has timed out --this is useful for detecting word
+     * spaces
+     */
+    bool has_timed_out() {
+        return (ubit.systemTime() - this->last_update) >=
+                morse_decoder.suggested_timeout_duration();
+    }
+
     // instance method event handler for UP/DOWN events on button A
     // TODO: Shorten this event-handler! It seems to be doing far too much!
     void on_button_a_change(MicroBitEvent e) {
@@ -74,7 +396,7 @@ public:
                     this->last_update = ubit.systemTime();
                     this->current_state = false;
                     // send the duration of the previous pulse to the decoder
-                    decoder.transition(true, this->last_update - previous_update);
+                    morse_decoder.transition(true, this->last_update - previous_update);
                     // invert middle pixel
                     ubit.display.image.setPixelValue(2, 2, 255 - ubit.display.image.getPixelValue(2, 2));
                 }
@@ -86,7 +408,7 @@ public:
                     this->last_update = ubit.systemTime();
                     this->current_state = true;
                     // send the duration of the previous pulse to the decoder
-                    decoder.transition(false, this->last_update - previous_update);
+                    morse_decoder.transition(false, this->last_update - previous_update);
                     // invert middle pixel
                     ubit.display.image.setPixelValue(2, 2, 255 - ubit.display.image.getPixelValue(2, 2));
                 }
@@ -103,32 +425,281 @@ private:
 };
 
 
+/**
+ * @returns true if a ≈ b, within ±margin% of b (where margin = 1.0 is 100%)
+ */
+template <typename T>
+bool is_roundabout(T a, T b, double margin) {
+    T max_bound = b * (1.0 + margin);
+    T min_bound = b * (1.0 - margin);
+    return min_bound <= a && a <= max_bound;
+}
+
+
+/**
+ * @brief Monitors the Line (pin input) for pulses, decodes them into bits and
+ * sends them into the ManchesterDecoder.
+ */
+class LineMonitor {
+public:
+    /**
+     * @brief Default constructor
+     * @param decoder ManchesterDecoder instance to send the input bits into
+     */
+    LineMonitor(ManchesterDecoder& decoder)
+        : decoder(decoder)
+        , reception_in_progress(false)
+        {}
+
+    /**
+     * @brief Initialises this LineMonitor, including attaching event-handler
+     */
+    void init() {
+        // set input pin to generate pule events speicifically
+        ubit.io.pin[COMMS_PIN_NUMBER].eventOn(MICROBIT_PIN_EVENT_ON_PULSE);
+        // set up event handler(s) for both HI and LO pulse events
+        ubit.messageBus.listen(
+            COMMS_PIN_EVENT_ID,
+            MICROBIT_PIN_EVT_PULSE_HI,
+            this,
+            &LineMonitor::on_pulse
+        );
+        ubit.messageBus.listen(
+            COMMS_PIN_EVENT_ID,
+            MICROBIT_PIN_EVT_PULSE_LO,
+            this,
+            &LineMonitor::on_pulse
+        );
+    }
+
+    /**
+     * @brief Tears down this LineMonitor, including removing event-handler
+     */
+    void stop() {
+        // block all input events on the comms pin
+        ubit.io.pin[COMMS_PIN_NUMBER].eventOn(MICROBIT_PIN_EVENT_NONE);
+        // tear down event handlers
+        ubit.messageBus.ignore(
+            COMMS_PIN_EVENT_ID,
+            MICROBIT_PIN_EVT_PULSE_HI,
+            this,
+            &LineMonitor::on_pulse
+        );
+        ubit.messageBus.ignore(
+            COMMS_PIN_EVENT_ID,
+            MICROBIT_PIN_EVT_PULSE_LO,
+            this,
+            &LineMonitor::on_pulse
+        );
+    }
+
+    /**
+     * @brief Event-handler for input pin pulse HI or LO events
+     */
+    void on_pulse(MicroBitEvent e) {
+        // create a new Pulse and store it in the buffer
+        Pulse p = {
+            e.value == MICROBIT_PIN_EVT_PULSE_LO ? false : true, // state
+            e.timestamp // duration
+        };
+        // if it's a long silence, this is a good time to clear input buffer
+        if (p.state == false and p.duration > TransceiverConfig::MAXIMUM_PULSE_DURATION) {
+            this->decoder.clear(); // this helps mitigate bit-slip errors persisting
+            // ubit.serial.send("Clear Buffer\r\n");
+        }
+        // otherwise only act on pulses which are shorter than a certain duration
+        if (p.duration > TransceiverConfig::MAXIMUM_PULSE_DURATION) return;
+        // ubit.serial.printf("on_pulse() -> { %i, %lu, }\r\n", p.state, p.duration);
+        this->buffer.add(p);
+    }
+
+    /**
+     * @brief Decodes all currently-buffered input pulses.
+     * @details Call this regularly to keep processing input data and keep in
+     * sync.
+     * STOP bits
+     */
+    void flush() {
+        bool has_output = false;
+        for (size_t i = 0; i < this->buffer.length(); i++) {
+            Pulse p = this->buffer[i];
+            // attempt to decode the Pulse
+            // handle all remaining HI/LO bits according to duration
+            if (
+                is_roundabout(
+                    p.duration,
+                    TransceiverConfig::SIGNAL_BIT_DURATION * 2, // double-length bits
+                    TransceiverConfig::DURATION_MARGIN
+                )
+            ) {
+                // send two inputs of the given state
+                has_output = true;
+                // ubit.serial.printf("%d%d", p.state, p.state);
+                this->decoder.input(p.state);
+                this->decoder.input(p.state);
+            } else if (
+                is_roundabout(
+                    p.duration,
+                    TransceiverConfig::SIGNAL_BIT_DURATION, // single-length bits
+                    TransceiverConfig::DURATION_MARGIN
+                )
+            ) {
+                // send just one input of the given state
+                has_output = true;
+                // ubit.serial.printf("%d", p.state);
+                this->decoder.input(p.state);
+            }
+        }
+        if (has_output) {
+            // ubit.serial.send("<- MANCHEDECODE in\r\n");
+        }
+        // empty the buffer
+        this->buffer.clear();
+    }
+private:
+    ManchesterDecoder& decoder; /**< ManchesterDecoder to send bits into */
+
+    bool reception_in_progress; /**< Whether a transmission is currently being received */
+
+    CircularQueueBuffer<Pulse, 72u> buffer; // buffer with 72 spaces available
+};
+
+
+/**
+ * @brief The opposite of LineMonitor, this class takes bit stream input and
+ * switches the Comms pin state accordingly to transmit the stream of bits out
+ * on the Line.
+ */
+class LineDriver {
+public:
+    /**
+     * @brief Default constructor
+     */
+    LineDriver()
+        {}
+
+    /**
+     * @brief Send an input bit into the LineDriver
+     * @param state the input bit to send
+     * @details the input is not sent immediately but queued for sending. Call
+     * flush() to send all bits when buffer is ready.
+     * @returns true if bit could be added to buffer
+     * @returns false if buffer is full
+     */
+    bool input(bool state) {
+        return this->buffer.add(state);
+    }
+
+    /**
+     * @brief Flushes the output buffer out to the Line
+     * @note This method will block the caller!
+     * @returns true if buffer flushed
+     * @returns false if buffer isn't big enough yet (can't flush)
+     * @todo Consider using more accurate timing than just sleep()
+     */
+    bool flush() {
+        if (this->buffer.length() < 16u) return false;
+        // START BIT --should be inverted version of first bit
+        ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(not this->buffer[0]);
+        ubit.sleep(TransceiverConfig::STOP_START_BIT_DURATION_MS);
+        // BEGIN DATA BITS
+        for (size_t i = 0; i < 16u; i++) {
+            ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(this->buffer[i]);
+            ubit.sleep(TransceiverConfig::SIGNAL_BIT_DURATION_MS);
+        }
+        // END DATA BITS
+        // STOP BIT --should be inverted version of last bit
+        ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(not this->buffer[15u]);
+        ubit.sleep(TransceiverConfig::STOP_START_BIT_DURATION_MS);
+        // drive low then switch to input mode
+        ubit.io.pin[COMMS_PIN_NUMBER].setDigitalValue(0);
+        ubit.io.pin[COMMS_PIN_NUMBER].getDigitalValue();
+        // remove transmitted bits
+        this->buffer.shrink_from_front(16u);
+        return true; // no problem
+    }
+private:
+    CircularQueueBuffer<bool, 64u> buffer; // buffer with enough space for 4 bytes
+};
+
+
+// this will track the button presses
+ButtonTracker button_tracker;
+// this is used to encode bytes we want to send to go out on the line
+ManchesterEncoder manchester_encoder;
+// this switches the line state
+LineDriver line_driver;
+// this is used to decode bytes from the line
+ManchesterDecoder manchester_decoder;
+// this monitors the line for changes
+LineMonitor line_monitor(manchester_decoder);
+
+
+// flag used by the morse_checker() fiber function to check if it should run
+bool morse_checker_enabled = false;
+
+
 // simple fiber used for checking Morse decode state (for now), will send in future
 void morse_checker() {
     char current = '#'; // currently-seen char from Morse input
     while (true) {
         ubit.sleep(100); // yield the CPU for a bit
+        // guard clause preventing execution of the remainder unless enabled
+        if (not morse_checker_enabled) continue;
+        // if input has timed out, send a word space transition
+        if (button_tracker.has_timed_out()) {
+            morse_decoder.transition(false, morse_decoder.suggested_timeout_duration());
+        }
         char previous = current; // track what last-seen char was
-        if (decoder.remove_next(current)) { // if we could read another Morse code
+        if (morse_decoder.remove_next(current)) { // if we could read another Morse code
             // current has now updated, so display this new char
-            if (current != previous) {
-                ubit.display.printCharAsync(current);
-            }
+            ubit.display.printCharAsync(current, 500);
+            // whenever another character is removed, send it down the line
+            /*
+             * NOTE: for now, just sending characters as-is but in the future
+             * they will be encoded rather than sent raw
+             */
+            // ubit.serial.printf("Morse Input: %c\r\n", current);
+            manchester_encoder.input(current); // NOTE: Might need cast
+            ubit.display.clear();
         } else {
             // otherwise, peek into what the code appears to be thus far
-            char peek = decoder.peek();
+            char peek = morse_decoder.peek();
             // if this peek is different to what it previously was, display
             if (peek != current) {
                 current = peek;
-                ubit.display.printCharAsync(current);
+                ubit.display.printChar(current);
             }
         }
     }
 }
 
 
-// set up event handler(s) --using a class to do this
-ButtonTracker button_tracker;
+// simple fiber that simply passes data out of encoder and into the line driver
+void flush_output() {
+    while (true) {
+        ubit.sleep(10); // yield the CPU for a bit
+        int next_state = 0;
+        bool printed = false;
+        while (manchester_encoder.get_next_state(next_state)) {
+            printed = true;
+            // ubit.serial.printf("%i", next_state);
+            line_driver.input(next_state);
+        }
+        if (printed) {
+            // ubit.serial.send(" <- MANCHECODE out\r\n");
+        }
+    }
+}
+
+
+// simple fiber that simply flushes data out of line monitor and into decoder
+void flush_input() {
+    while (true) {
+        ubit.sleep(10); // yield the CPU for a bit
+        line_monitor.flush();
+    }
+}
 
 
 int main() {
@@ -138,16 +709,84 @@ int main() {
     // greyscale mode please!
     ubit.display.setDisplayMode(DISPLAY_MODE_GREYSCALE);
 
-    // ButtonTracker doesn't seem to work when instantiated in main()
-    // ButtonTracker button_tracker;
-
-    // create one fiber --this infintely checks the decoder and outputs changes
-    create_fiber(morse_checker);
+    // these lightweight fibers ferry data to and from decoders and line stuff
 
-    // TODO: consider tearing down event handlers?
+    // this is the main program loop, an infinite while!
     while (true) {
-        // forever sleep, earnt it I have
-        ubit.sleep(1000);
+        // first of all, work out what Role we are to play
+        Role my_role = get_my_role(); // this call blocks and asks the user
+
+        Pairer pairer;
+
+        // try to pair and if unsuccessful, go back to top of loop to restart
+        int error_code = pairer.attempt_pair(my_role);
+        if (error_code != 0) {
+            // scroll "PF" to indicate "PAIR FAILURE"
+            ubit.display.scroll("PF");
+            ubit.display.scroll(error_code);
+            continue; // skip back to start of loop body to re-try pair
+        }
+        // otherwise, if execution continues here then we're paired
+
+        // for now, we only act on button presses if we're the Master
+        if (my_role == Role::MASTER) {
+            // scroll "PM" to mean "PAIRED MASTER"
+            // ubit.serial.send(" -> Role::MASTER\r\n");
+            ubit.display.scroll("PM");
+        } else if (my_role == Role::SLAVE) {
+            // scroll "PS" to mean "PAIRED SLAVE"
+            // ubit.serial.send(" -> Role::SLAVE\r\n");
+            ubit.display.scroll("PS");
+        }
+
+        /* BEGIN COMMS */
+
+        if (my_role == Role::MASTER) { // Master's comms loop
+            // initialise the ButtonTracker
+            button_tracker.init();
+            // create one fiber --this infinitely checks the decoder and outputs changes
+            create_fiber(morse_checker);
+            // enable the morse checker!
+            morse_checker_enabled = true;
+            create_fiber(flush_output);
+            // line_driver.init();
+            while (true) { // TODO: consider what exit conditions can be used
+                // call barrier to synchronise both master and slave
+                pairer.barrier();
+                // flush the line driver
+                line_driver.flush();
+            }
+        } else { // Slave's comms loop
+            create_fiber(flush_input);
+            line_monitor.init();
+            while (true) { // TODO: consider what exit conditions can be used
+                // call barrier to synchronise both master and slave
+                pairer.barrier();
+                // TODO: display the output?
+                uint8_t next_byte = 0x00u;
+                if (manchester_decoder.get_next_byte(next_byte)) {
+                    // ubit.serial.printf("MANCHEDECODE -> %c\r\n", next_byte);
+                    ubit.display.printChar(next_byte, 300);
+                    ubit.display.clear();
+                }
+            }
+        }
+
+        /* END COMMS */
+
+        /*
+         * NOTE: the close-down code here is currently unreachable but will be
+         * in the future
+         */
+
+        // transceiver.close();
+
+        // if we're the Master, we need to stop the button tracker
+        if (my_role == Role::MASTER) {
+            // button_tracker.stop();
+            // disable the morse checker
+            morse_checker_enabled = false;
+        }
     }
 
     // TODO: potentially remove this call, if clarified that it is not required.
diff --git a/tests/manchester_encoder_decoder_test.cpp b/tests/manchester_encoder_decoder_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c6d6ca03d00a11a583bc5df82c0e9699bd250ce8
--- /dev/null
+++ b/tests/manchester_encoder_decoder_test.cpp
@@ -0,0 +1,80 @@
+// XXX: this is a basic test program of the ManchesterEncoder class, debug only
+
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+
+#include "../source/ManchesterDecoder.hpp"
+#include "../source/ManchesterEncoder.hpp"
+
+
+int main() {
+    using namespace com_saxbophone;
+
+    struct TestCase {
+        uint8_t input; // byte being sent into the encoder
+        bool output[16]; // sequence of HIGH/LOW states to be used as output
+    };
+
+    // test cases for the encoder
+    TestCase test_cases[] = {
+        { 0x00u, { 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, }, },
+        { 0x73u, { 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, }, },
+        { 0x54u, { 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, }, },
+        { 0xACu, { 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, }, },
+    };
+
+    // test each test case
+    for (TestCase t : test_cases) {
+        ManchesterEncoder encoder;
+        ManchesterDecoder decoder;
+        encoder.input(t.input);
+        printf("Input:   \t%x\n", t.input);
+        printf("Expected:\t");
+        for (size_t i = 0; i < 16; i++) {
+            printf("%u", t.output[i]);
+            fflush(stdout);
+        }
+        printf("\n");
+        printf("Actual:  \t");
+        for (size_t i = 0; i < 16; i++) {
+            int state = 0;
+            encoder.get_next_state(state);
+            assert(state == t.output[i]);
+            decoder.input(state);
+            printf("%u", state);
+            fflush(stdout);
+        }
+        printf("\n");
+        uint8_t decoded = 0xff;
+        decoder.get_next_byte(decoded);
+        printf("Decoded: \t%x\n", decoded);
+        assert(decoded == t.input);
+        // draw a bar
+        printf("================================\n");
+    }
+
+    // additional test cases of every byte
+    printf("IN -> MANCHESTER CODE -> OUT\n");
+    ManchesterEncoder encoder;
+    ManchesterDecoder decoder;
+    uint8_t b = 0x00u;
+    do {
+        printf("%02x -> ", b);
+        encoder.input(b);
+        for (uint8_t i = 0; i < 16; i++) {
+            int symbol;
+            encoder.get_next_state(symbol);
+            printf("%d", symbol);
+            decoder.input(symbol);
+        }
+        uint8_t output = 0xffu;
+        decoder.get_next_byte(output);
+        printf(" -> %02x\n", output);
+        assert(output == b);
+        b++;
+    } while(b != 0x00u); // detect unsigned wrap-around
+
+    return 0;
+}
diff --git a/tests/morse_decoder_test.cpp b/tests/morse_decoder_test.cpp
index 4f07b08e297593da2c289c2f7c1500cfa976b0f4..31820dd099e4ae8f5fcb357511e016ad799f074a 100644
--- a/tests/morse_decoder_test.cpp
+++ b/tests/morse_decoder_test.cpp
@@ -8,7 +8,7 @@
 int main() {
     using namespace com_saxbophone;
 
-    MorseDecoder<> decoder(100);
+    MorseDecoder decoder(100);
 
     struct Input {
         bool state; // whether it's on (button held) or off (button released)