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)