From 8f4367222e4c2f1ffaf3e3fdf2e2da85f34b3727 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 28 Feb 2018 07:13:56 -0500 Subject: [PATCH 1/9] add state value for ascii encoding offset in case it is needed --- cpp/arduino/PinHistory.h | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cpp/arduino/PinHistory.h b/cpp/arduino/PinHistory.h index 5a841a14..7bd8c56c 100644 --- a/cpp/arduino/PinHistory.h +++ b/cpp/arduino/PinHistory.h @@ -58,7 +58,13 @@ class PinHistory { } public: - PinHistory() {} + unsigned int asciiEncodingOffsetIn; + unsigned int asciiEncodingOffsetOut; + + PinHistory() { + asciiEncodingOffsetIn = 0; // default is sensible + asciiEncodingOffsetOut = 1; // default is sensible + } void reset(T val) { clear(); @@ -108,11 +114,19 @@ class PinHistory { // convert the queue of incoming data to a string as if it was Serial comms // start from offset, consider endianness - String incomingToAscii (unsigned int offset, bool bigEndian) const { return q2a(qIn, offset, bigEndian); } + String incomingToAscii(unsigned int offset, bool bigEndian) const { return q2a(qIn, offset, bigEndian); } + + // convert the queue of incoming data to a string as if it was Serial comms + // start from offset, consider endianness + String incomingToAscii(bool bigEndian) const { return incomingToAscii(asciiEncodingOffsetIn, bigEndian); } + + // convert the pin history to a string as if it was Serial comms + // start from offset, consider endianness + String toAscii(unsigned int offset, bool bigEndian) const { return q2a(qOut, offset, bigEndian); } // convert the pin history to a string as if it was Serial comms // start from offset, consider endianness - String toAscii (unsigned int offset, bool bigEndian) const { return q2a(qOut, offset, bigEndian); } + String toAscii(bool bigEndian) const { return toAscii(asciiEncodingOffsetOut, bigEndian); } // copy elements to an array, up to a given length // return the number of elements moved From 1761fb0a6370358bb2329b0137726453399dfe0f Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 28 Feb 2018 07:19:05 -0500 Subject: [PATCH 2/9] since Queue is a common data structure, put it in a special dir from arduino mocks to avoid conflicts --- SampleProjects/TestSomething/test/queue.cpp | 2 +- cpp/arduino/PinHistory.h | 2 +- cpp/arduino/{ => ci}/Queue.h | 0 cpp/arduino/ci/README.md | 5 +++++ 4 files changed, 7 insertions(+), 2 deletions(-) rename cpp/arduino/{ => ci}/Queue.h (100%) create mode 100644 cpp/arduino/ci/README.md diff --git a/SampleProjects/TestSomething/test/queue.cpp b/SampleProjects/TestSomething/test/queue.cpp index aa367317..5b585234 100644 --- a/SampleProjects/TestSomething/test/queue.cpp +++ b/SampleProjects/TestSomething/test/queue.cpp @@ -1,5 +1,5 @@ #include -#include +#include unittest(basic_queue_dequeue_and_size) { diff --git a/cpp/arduino/PinHistory.h b/cpp/arduino/PinHistory.h index 7bd8c56c..e4075ddb 100644 --- a/cpp/arduino/PinHistory.h +++ b/cpp/arduino/PinHistory.h @@ -1,5 +1,5 @@ #pragma once -#include "Queue.h" +#include "ci/Queue.h" #include "WString.h" // pins with history. diff --git a/cpp/arduino/Queue.h b/cpp/arduino/ci/Queue.h similarity index 100% rename from cpp/arduino/Queue.h rename to cpp/arduino/ci/Queue.h diff --git a/cpp/arduino/ci/README.md b/cpp/arduino/ci/README.md new file mode 100644 index 00000000..19831d42 --- /dev/null +++ b/cpp/arduino/ci/README.md @@ -0,0 +1,5 @@ +The parent directory is for files that must stand in for their Arduino counterparts -- any `SomeFile` that might be requested as `#include `. + +This directory is specificially for support files required by those other files. That's because we don't want to create collisions on filenames for common data structures like Queue. + +If there end up being class-level conflicts, it is this developer's stated intention to rename our classes such that `class Float` becomes `class FloatyMcFloatFace`. From 62d8d1b567884df19ce856e9a2fa6e4777ee9461 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Wed, 28 Feb 2018 19:12:14 -0500 Subject: [PATCH 3/9] serial comms are little-endian on the bit level --- .../TestSomething/test/softwareserial.cpp | 15 +++++++++------ cpp/arduino/SoftwareSerial.h | 10 ++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/SampleProjects/TestSomething/test/softwareserial.cpp b/SampleProjects/TestSomething/test/softwareserial.cpp index a168676b..4423df2f 100644 --- a/SampleProjects/TestSomething/test/softwareserial.cpp +++ b/SampleProjects/TestSomething/test/softwareserial.cpp @@ -1,17 +1,20 @@ #include #include +bool bigEndian = false; +bool flipLogic = false; + unittest(software_input_output) { GodmodeState* state = GODMODE(); state->reset(); - SoftwareSerial ss(1, 2, false); + SoftwareSerial ss(1, 2, flipLogic); assertEqual(-1, ss.peek()); - state->digitalPin[1].fromAscii("Holy crap ", true); - state->digitalPin[1].fromAscii("this took a lot of prep work", true); + state->digitalPin[1].fromAscii("Holy crap ", bigEndian); + state->digitalPin[1].fromAscii("this took a lot of prep work", bigEndian); assertFalse(ss.isListening()); assertEqual(-1, ss.peek()); @@ -28,17 +31,17 @@ unittest(software_input_output) ss.write('b'); ss.write('A'); ss.write('r'); - assertEqual("bAr", state->digitalPin[2].toAscii(1, true)); + assertEqual("bAr", state->digitalPin[2].toAscii(1, bigEndian)); } unittest(print) { GodmodeState* state = GODMODE(); state->reset(); - SoftwareSerial ss(1, 2, false); + SoftwareSerial ss(1, 2, flipLogic); ss.listen(); ss.print(1.3, 2); - assertEqual("1.30", state->digitalPin[2].toAscii(1, true)); + assertEqual("1.30", state->digitalPin[2].toAscii(1, bigEndian)); } unittest_main() diff --git a/cpp/arduino/SoftwareSerial.h b/cpp/arduino/SoftwareSerial.h index 0fbdfe75..f2d567b0 100644 --- a/cpp/arduino/SoftwareSerial.h +++ b/cpp/arduino/SoftwareSerial.h @@ -14,6 +14,7 @@ class SoftwareSerial : public Stream bool mIsListening; GodmodeState* mState; unsigned long mOffset; // bits to offset stream + bool bigEndian; public: SoftwareSerial(uint8_t receivePin, uint8_t transmitPin, bool inverse_logic = false) { @@ -22,6 +23,7 @@ class SoftwareSerial : public Stream mIsListening = false; mOffset = 0; // godmode starts with 1 bit in the queue mState = GODMODE(); + bigEndian = false; // this is how serial works } ~SoftwareSerial() {}; @@ -43,14 +45,14 @@ class SoftwareSerial : public Stream int peek() { if (!isListening()) return -1; - String input = mState->digitalPin[mPinIn].incomingToAscii(mOffset, true); + String input = mState->digitalPin[mPinIn].incomingToAscii(mOffset, bigEndian); if (input.empty()) return -1; return input[0]; } virtual int read() { if (!isListening()) return -1; - String input = mState->digitalPin[mPinIn].incomingToAscii(mOffset, true); + String input = mState->digitalPin[mPinIn].incomingToAscii(mOffset, bigEndian); if (input.empty()) return -1; int ret = input[0]; for (int i = 0; i < 8; ++i) digitalRead(mPinIn); @@ -60,11 +62,11 @@ class SoftwareSerial : public Stream //using Print::write; virtual size_t write(uint8_t byte) { - mState->digitalPin[mPinOut].outgoingFromAscii(String((char)byte), true); + mState->digitalPin[mPinOut].outgoingFromAscii(String((char)byte), bigEndian); return 1; } - virtual int available() { return mState->digitalPin[mPinIn].incomingToAscii(mOffset, true).length(); } + virtual int available() { return mState->digitalPin[mPinIn].incomingToAscii(mOffset, bigEndian).length(); } virtual void flush() {} operator bool() { return true; } From 7ea2483b9c089c06af3b30bd9019d26827d81579 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Sun, 4 Mar 2018 14:37:12 -0500 Subject: [PATCH 4/9] assureTrue/false now assures instead of asserting --- CHANGELOG.md | 1 + cpp/unittest/Assertion.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca1c09c..b9f8c25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `arduino_ci_remote.rb` no longer makes unnecessary changes to the board being tested - Scripts no longer crash if there is no `test/` directory - Scripts no longer crash if there is no `examples/` directory +- `assureTrue` and `assureFalse` now `assure` instead of just `assert`ing. ### Security diff --git a/cpp/unittest/Assertion.h b/cpp/unittest/Assertion.h index a4b8d608..1e9e6b16 100644 --- a/cpp/unittest/Assertion.h +++ b/cpp/unittest/Assertion.h @@ -46,6 +46,6 @@ #define assureMore(arg1,arg2) assureOp("assureMore","upperBound",arg1,compareMore,">","lowerBound",arg2) #define assureLessOrEqual(arg1,arg2) assureOp("assureLessOrEqual","lowerBound",arg1,compareLessOrEqual,"<=","upperBound",arg2) #define assureMoreOrEqual(arg1,arg2) assureOp("assureMoreOrEqual","upperBound",arg1,compareMoreOrEqual,">=","lowerBound",arg2) -#define assureTrue(arg) assertEqual(true, arg) -#define assureFalse(arg) assertEqual(false, arg) +#define assureTrue(arg) assureEqual(true, arg) +#define assureFalse(arg) assureEqual(false, arg) From 28d83e78e33741a7f6e15361c8f81d70b9420c98 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 5 Mar 2018 06:49:27 -0500 Subject: [PATCH 5/9] Add FlashStringHelper compilation --- CHANGELOG.md | 1 + cpp/arduino/Print.h | 6 ++- cpp/arduino/WString.h | 6 +++ cpp/arduino/avr/pgmspace.h | 94 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 cpp/arduino/avr/pgmspace.h diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f8c25f..24684c68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Yaml files support select/reject critera for paths of unit tests for targeted testing - Pins now track history and can report it in Ascii (big- or little-endian) for digital sequences - Pins now accept an array (or string) of input bits for providing pin values across multiple reads +- FlashStringHelper (and related macros) compilation mocks - SoftwareSerial. That took a while. ### Changed diff --git a/cpp/arduino/Print.h b/cpp/arduino/Print.h index 66c1ff06..c1dcdde1 100644 --- a/cpp/arduino/Print.h +++ b/cpp/arduino/Print.h @@ -31,8 +31,10 @@ class Print virtual size_t write(uint8_t) = 0; size_t write(const char *str) { return str == NULL ? 0 : write((const uint8_t *)str, String(str).length()); } - virtual size_t write(const uint8_t *buffer, size_t size) - { + + size_t write(const __FlashStringHelper *str) { return write((const char *)str); } + + virtual size_t write(const uint8_t *buffer, size_t size) { size_t n; for (n = 0; size && write(*buffer++) && ++n; --size); return n; diff --git a/cpp/arduino/WString.h b/cpp/arduino/WString.h index 50840dfd..8e362a30 100644 --- a/cpp/arduino/WString.h +++ b/cpp/arduino/WString.h @@ -9,6 +9,9 @@ typedef std::string string; +//typedef const char __FlashStringHelper; +class __FlashStringHelper; +#define F(string_literal) (reinterpret_cast(PSTR(string_literal))) // Compatibility with string class class String: public string @@ -51,6 +54,7 @@ class String: public string public: ~String(void) {} + String(const __FlashStringHelper *str): string((const char *)str) {} String(const char *cstr = ""): string(cstr) {} String(const string &str): string(str) {} String(const String &str): string(str) {} @@ -70,6 +74,7 @@ class String: public string String & operator = (const char *cstr) { assign(cstr); return *this; } String & operator = (const char c) { assign(1, c); return *this; } + unsigned char concat(const __FlashStringHelper *str) { append((const char *)str); return 1; } unsigned char concat(const String &str) { append(str); return 1; } unsigned char concat(const char *cstr) { append(cstr); return 1; } unsigned char concat(char c) { append(1, c); return 1; } @@ -81,6 +86,7 @@ class String: public string unsigned char concat(float num) { append(String(num)); return 1; } unsigned char concat(double num) { append(String(num)); return 1; } + String & operator += (const __FlashStringHelper *rhs) { concat(rhs); return *this; } String & operator += (const String &rhs) { concat(rhs); return *this; } String & operator += (const char *cstr) { concat(cstr); return *this; } String & operator += (char c) { concat(c); return *this; } diff --git a/cpp/arduino/avr/pgmspace.h b/cpp/arduino/avr/pgmspace.h new file mode 100644 index 00000000..cb83e57c --- /dev/null +++ b/cpp/arduino/avr/pgmspace.h @@ -0,0 +1,94 @@ +#pragma once + + +/* +def d(var_raw) + var = var_raw.split("_")[0] + out = "#define #{var}_P(...) #{var}(__VA_ARGS__)\n" + IO.popen('pbcopy', 'w') { |f| f << out } + out + end + +text = File.open("arduino-1.8.5/hardware/tools/avr/avr/include/avr/pgmspace.h").read +externs = text.split("\n").select {|l| l.start_with? "extern"} +out = externs.map {|l| l.split("(")[0].split(" ")[-1].gsub("*", "") }.uniq +out.each { |l| puts d(l) } +*/ + +#include + +#define PROGMEM + +#ifndef PGM_P +#define PGM_P const char * +#endif + +#ifndef PGM_VOID_P +#define PGM_VOID_P const void * +#endif + +// everything's a no-op +#define PSTR(s) ((const char *)(s)) +#define pgm_read_byte_near(x) (x) +#define pgm_read_word_near(x) (x) +#define pgm_read_dword_near(x) (x) +#define pgm_read_float_near(x) (x) +#define pgm_read_ptr_near(x) (x) + +#define pgm_read_byte_far(x) (x) +#define pgm_read_word_far(x) (x) +#define pgm_read_dword_far(x) (x) +#define pgm_read_float_far(x) (x) +#define pgm_read_ptr_far(x) (x) + + +#define pgm_read_byte(x) (x) +#define pgm_read_word(x) (x) +#define pgm_read_dword(x) (x) +#define pgm_read_float(x) (x) +#define pgm_read_ptr(x) (x) +#define pgm_get_far_address(x) (x) + +#define memchr_P(...) memchr(__VA_ARGS__) +#define memcmp_P(...) memcmp(__VA_ARGS__) +#define memccpy_P(...) memccpy(__VA_ARGS__) +#define memcpy_P(...) memcpy(__VA_ARGS__) +#define memmem_P(...) memmem(__VA_ARGS__) +#define memrchr_P(...) memrchr(__VA_ARGS__) +#define strcat_P(...) strcat(__VA_ARGS__) +#define strchr_P(...) strchr(__VA_ARGS__) +#define strchrnul_P(...) strchrnul(__VA_ARGS__) +#define strcmp_P(...) strcmp(__VA_ARGS__) +#define strcpy_P(...) strcpy(__VA_ARGS__) +#define strcasecmp_P(...) strcasecmp(__VA_ARGS__) +#define strcasestr_P(...) strcasestr(__VA_ARGS__) +#define strcspn_P(...) strcspn(__VA_ARGS__) +#define strlcat_P(...) strlcat(__VA_ARGS__) +#define strlcpy_P(...) strlcpy(__VA_ARGS__) +#define strnlen_P(...) strnlen(__VA_ARGS__) +#define strncmp_P(...) strncmp(__VA_ARGS__) +#define strncasecmp_P(...) strncasecmp(__VA_ARGS__) +#define strncat_P(...) strncat(__VA_ARGS__) +#define strncpy_P(...) strncpy(__VA_ARGS__) +#define strpbrk_P(...) strpbrk(__VA_ARGS__) +#define strrchr_P(...) strrchr(__VA_ARGS__) +#define strsep_P(...) strsep(__VA_ARGS__) +#define strspn_P(...) strspn(__VA_ARGS__) +#define strstr_P(...) strstr(__VA_ARGS__) +#define strtok_P(...) strtok(__VA_ARGS__) +#define strtok_P(...) strtok(__VA_ARGS__) +#define strlen_P(...) strlen(__VA_ARGS__) +#define strnlen_P(...) strnlen(__VA_ARGS__) +#define memcpy_P(...) memcpy(__VA_ARGS__) +#define strcpy_P(...) strcpy(__VA_ARGS__) +#define strncpy_P(...) strncpy(__VA_ARGS__) +#define strcat_P(...) strcat(__VA_ARGS__) +#define strlcat_P(...) strlcat(__VA_ARGS__) +#define strncat_P(...) strncat(__VA_ARGS__) +#define strcmp_P(...) strcmp(__VA_ARGS__) +#define strncmp_P(...) strncmp(__VA_ARGS__) +#define strcasecmp_P(...) strcasecmp(__VA_ARGS__) +#define strncasecmp_P(...) strncasecmp(__VA_ARGS__) +#define strstr_P(...) strstr(__VA_ARGS__) +#define strlcpy_P(...) strlcpy(__VA_ARGS__) +#define memcmp_P(...) memcmp(__VA_ARGS__) From 2e7436ac328cc86eccfb2e102af6ff5ff40e98ac Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 5 Mar 2018 07:06:47 -0500 Subject: [PATCH 6/9] Add templatized Table implementation --- CHANGELOG.md | 2 + SampleProjects/TestSomething/test/table.cpp | 74 ++++++++++++ cpp/arduino/ci/Table.h | 125 ++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 SampleProjects/TestSomething/test/table.cpp create mode 100644 cpp/arduino/ci/Table.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 24684c68..d3dc197d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Pins now accept an array (or string) of input bits for providing pin values across multiple reads - FlashStringHelper (and related macros) compilation mocks - SoftwareSerial. That took a while. +- Queue template implementation +- Table template implementation ### Changed - Unit test executables print to STDERR just in case there are segfaults. Uh, just in case I ever write any. diff --git a/SampleProjects/TestSomething/test/table.cpp b/SampleProjects/TestSomething/test/table.cpp new file mode 100644 index 00000000..712b649d --- /dev/null +++ b/SampleProjects/TestSomething/test/table.cpp @@ -0,0 +1,74 @@ +#include +#include +#include + +// for testing a work function +// note swapped args because as "isMatch", it's "firstArg, key" +bool isSubstr(String firstArg, String haystack) { + return strstr(haystack.c_str(), firstArg.c_str()); +} + +// for testing a work function +int results[5]; +void setResult2(int k, int v) { + results[k] = v; +} +void setResult3(long l, int k, int v) { + results[k] = v + l; +} + + +unittest(basic_table) +{ + Table t; + assertTrue(t.empty()); + + int data[5] = {11, 22, 33, 44, 55}; + + for (int i = 0; i < 5; ++i) { + assertEqual(i, t.size()); + assertTrue(t.add(String(data[i]), data[i])); + assertEqual(i + 1, t.size()); + } + + assertTrue(t.has("44")); + assertFalse(t.has("66")); + assertEqual(44, t.get("44")); + + assertEqual("33", t.getMatchingKey(String("3"), isSubstr)); + + for (int i = 0; i < 5; ++i) { + assertEqual(5 - i, t.size()); + assertTrue(t.remove(String(data[i]))); + assertFalse(t.has(String(data[i]))); + assertEqual(4 - i, t.size()); + } + +} + +unittest(iteration_no_arg) { + Table t; + for (int i = 0; i < 5; ++i) { + results[i] = 0; + t.add(i, 11 * (i + 1)); + } + + t.iterate(&setResult2); + + for (int i = 0; i < 5; ++i) assertEqual(11 * (i + 1), results[i]); +} + +unittest(iteration_one_arg) { + Table t; + for (int i = 0; i < 5; ++i) { + results[i] = 0; + t.add(i, 11 * (i + 1)); + } + + long offset = 9; + t.iterate(&setResult3, offset); + + for (int i = 0; i < 5; ++i) assertEqual(11 * (i + 1), results[i] - offset); +} + +unittest_main() diff --git a/cpp/arduino/ci/Table.h b/cpp/arduino/ci/Table.h new file mode 100644 index 00000000..f984a3c2 --- /dev/null +++ b/cpp/arduino/ci/Table.h @@ -0,0 +1,125 @@ +#pragma once + +// A template-ized lookup table implementation +// +// this is this stupidest table implementation ever but it's +// an MVP for unit testing. O(n). +template +class Table { + private: + struct Node { + K key; + V val; + Node *next; + }; + + Node* mStart; + unsigned long mSize; + // to alow const reference signatures, pre-allocate nil values + K mNilK; + V mNilV; + + void init() { + mStart = NULL; + mSize = 0; + } + + public: + Table() : mNilK(), mNilV() { init(); } + + // number of things in the table + inline unsigned long size() const { return mSize; } + + // whether there are no things + inline bool empty() const { return 0 == mSize; } + + // whether there is a thing stored at the given key + bool has(K const key) const { + for (Node* p = mStart; p; p = p->next) { + if (p->key == key) return true; + } + return false; + } + + // allow find operations on keys + template + const K& getMatchingKey(T const firstArg, bool (*isMatch)(const T, const K)) const { + for (Node* p = mStart; p; p = p->next) { + if (isMatch(firstArg, p->key)) return p->key; + } + return mNilK; + } + + // allow iteration over entire table, with a work function that takes key/value pairs + void iterate(void (*work)(const K&, const V&)) const { + for (Node* p = mStart; p; p = p->next) work(p->key, p->val); + } + void iterate(void (*work)(K, V)) const { + for (Node* p = mStart; p; p = p->next) work(p->key, p->val); + } + + // allow iteration over entire table, with a work function that takes key/value pairs + // plus an initial argument. this enables member function passing (via workaround) + template + void iterate(void (*work)(T&, const K&, const V&), T& firstArg) const { + for (Node* p = mStart; p; p = p->next) work(firstArg, p->key, p->val); + } + + template + void iterate(void (*work)(T&, K, V), T& firstArg) const { + for (Node* p = mStart; p; p = p->next) work(firstArg, p->key, p->val); + } + + template + void iterate(void (*work)(T, K, V), T firstArg) const { + for (Node* p = mStart; p; p = p->next) work(firstArg, p->key, p->val); + } + + // return the value for a given key + const V& get(K const key) const { + for (Node* p = mStart; p; p = p->next) { + if (p->key == key) return p->val; + } + return mNilV; + } + + // remove an item by key + bool remove(K const key) { + Node *o = NULL; + for (Node* p = mStart; p; p = p->next) { + if (p->key == key) { + (o ? o->next : mStart) = p->next; + delete p; + --mSize; + return true; + } + o = p; + } + return false; + } + + // add a key/value pair. deletes any existing key by that name. + bool add(K const key, V const val) { + remove(key); + Node *n = new Node; + if (n == NULL) return false; + n->key = key; + n->val = val; + n->next = mStart; + mStart = n; + ++mSize; + return true; + } + + // remove everything + void clear() { + Node* p; + while (mStart) { + p = mStart; + mStart = mStart->next; + delete p; + } + } + + ~Table() { clear(); } +}; From 80f4f6bafda306c0eddea319314c8eba3807ed56 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 5 Mar 2018 07:18:50 -0500 Subject: [PATCH 7/9] add ObservableDataStream and DataStreamObserver pattern implementation --- CHANGELOG.md | 1 + .../test/observabledatastream.cpp | 107 +++++++++++++++ cpp/arduino/ci/ObservableDataStream.h | 124 ++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 SampleProjects/TestSomething/test/observabledatastream.cpp create mode 100644 cpp/arduino/ci/ObservableDataStream.h diff --git a/CHANGELOG.md b/CHANGELOG.md index d3dc197d..d42ca9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - SoftwareSerial. That took a while. - Queue template implementation - Table template implementation +- ObservableDataStream and DataStreamObserver pattern implementation ### Changed - Unit test executables print to STDERR just in case there are segfaults. Uh, just in case I ever write any. diff --git a/SampleProjects/TestSomething/test/observabledatastream.cpp b/SampleProjects/TestSomething/test/observabledatastream.cpp new file mode 100644 index 00000000..6f1936b5 --- /dev/null +++ b/SampleProjects/TestSomething/test/observabledatastream.cpp @@ -0,0 +1,107 @@ +#include +#include +#include + +class Source : public ObservableDataStream { + public: + Source() : ObservableDataStream() {} + + // expose protected functions + void doBit(bool val) { advertiseBit(val); } + void doByte(unsigned char val) { advertiseByte(val); } +}; + +class Sink : public DataStreamObserver { + public: + bool lastBit; + unsigned char lastByte; + + Sink() : DataStreamObserver(false, false) {} + + virtual String observerName() const { return "Sink"; } + virtual void onBit(bool val) { lastBit = val; } + virtual void onByte(unsigned char val) { lastByte = val; } +}; + +class BitpackSink : public DataStreamObserver { + public: + bool lastBit; + unsigned char lastByte; + + BitpackSink() : DataStreamObserver(true, true) {} + + virtual String observerName() const { return "BitpackSink"; } + virtual void onBit(bool val) { lastBit = val; } + virtual void onByte(unsigned char val) { lastByte = val; } +}; + +unittest(attach_sink_to_src) +{ + Source src = Source(); + Sink dst = Sink(); + + dst.lastByte = 'z'; + src.addObserver("foo", &dst); + src.doByte('a'); + assertEqual('a', dst.lastByte); + src.removeObserver("foo"); + src.doByte('b'); + assertEqual('a', dst.lastByte); +} + +unittest(attach_src_to_sink) +{ + Source src = Source(); + Sink dst = Sink(); + + dst.attach(&src); + src.doByte('f'); + assertEqual('f', dst.lastByte); +} + +// 01010100 T if bigendian +unittest(bitpack) +{ + Source src = Source(); + Sink dst = Sink(); + BitpackSink bst = BitpackSink(); + + bool message[8] = {0, 1, 0, 1, 0, 1, 0, 0}; + + bst.lastByte = 'f'; + dst.lastByte = 'f'; + bst.attach(&src); + dst.attach(&src); + + for (int i = 0; i < 8; ++i) { + src.doBit(message[i]); + assertEqual(message[i], bst.lastBit); + assertEqual(message[i], dst.lastBit); + } + + assertEqual('f', dst.lastByte); // not doing bitpacking + assertEqual('T', bst.lastByte); // should have formed a binary T char by now + assertNotEqual('*', bst.lastByte); // backwards endianness +} + +// 01010100 T if bigendian +unittest(from_pinhistory) +{ + GodmodeState* state = GODMODE(); + state->reset(); + + BitpackSink bst = BitpackSink(); + bst.attach(&state->digitalPin[2]); + bst.lastByte = 'f'; + + bool message[8] = {0, 1, 0, 1, 0, 1, 0, 0}; + for (int i = 0; i < 8; ++i) { + digitalWrite(2, message[i]); + assertEqual(message[i], bst.lastBit); + } + + assertEqual('T', bst.lastByte); // should have formed a binary T char by now + assertNotEqual('*', bst.lastByte); // backwards endianness +} + +unittest_main() diff --git a/cpp/arduino/ci/ObservableDataStream.h b/cpp/arduino/ci/ObservableDataStream.h new file mode 100644 index 00000000..48723480 --- /dev/null +++ b/cpp/arduino/ci/ObservableDataStream.h @@ -0,0 +1,124 @@ +#pragma once + +#include "Table.h" +#include + + +// This pair of classes defines an Observer pattern for bits and bytes. +// This would allow us to create "devices" that respond in "real" time +// to Arduino outputs, in the form of altering the Arduino inputs +// +// e.g. replying to a serial output with serial input +class ObservableDataStream; + +// datastream observers handle deliveries of bits and bytes. +// optionally, they can turn bit events into byte events with a given endianness +class DataStreamObserver { + private: + unsigned int mBitPosition; // for building the byte (mask helper) + unsigned char mBuildingByte; // for storing incoming bits + bool mAutoBitPack; // whether to report the packed bits + bool mBigEndian; // bit order for byte + + protected: + // functions that are up to the implementer to provide. + virtual void onBit(bool aBit) {} + virtual void onByte(unsigned char aByte) {} + virtual String observerName() const = 0; + + public: + DataStreamObserver(bool autoBitPack, bool bigEndian) + { + mBitPosition = 0; + mBuildingByte = 0x00; + mAutoBitPack = autoBitPack; + mBigEndian = bigEndian; + } + + virtual ~DataStreamObserver() {} + + // entry point for byte-related handler + void handleByte(unsigned char aByte) { + onByte(aByte); + } + + // entry poitn for bit-related handler + void handleBit(bool aBit) { + onBit(aBit); + + if (!mAutoBitPack) return; + + // build the next value + int shift = mBigEndian ? 7 - mBitPosition : mBitPosition; + unsigned char val = aBit ? 0x1 : 0x0; + mBuildingByte |= (val << shift); + + // if we roll over after incrementing, the byte is ready to ship + mBitPosition = (mBitPosition + 1) % 8; + if (mBitPosition == 0) { + handleByte(mBuildingByte); + mBuildingByte = 0x00; + }; + } + + // inlined after ObservableDataStream definition to fake out the compiler + bool attach(ObservableDataStream* source); + bool detach(ObservableDataStream* source); +}; + +// Inheritable interface for things that produce data, like pins or serial ports +// this class allows others to subscribe for updates on these values and trigger actions +// e.g. if you "turn on" a motor with one pin and expect to see a change in an analog pin +class ObservableDataStream +{ + private: + Table mObservers; + bool mAdvertisingBit; + unsigned char mAdvertisingByte; + + protected: + // to allow both member and non-member functions to be called, we need to trick the compiler + // into getting the (this) of a static function. So the default is a work function signature + // that takes a second optional argument. in this case, we use the argument. + + static void workAdvertiseBit(ObservableDataStream* that, String _, DataStreamObserver* val) { + val->handleBit(that->mAdvertisingBit); + } + + static void workAdvertiseByte(ObservableDataStream* that, String _, DataStreamObserver* val) { + val->handleByte(that->mAdvertisingByte); + } + + // advertise functions allow the data stream to publish to observers + + // update all observers with a byte value + void advertiseByte(unsigned char aByte) { + // save the value to a class variable. then use the static method with this instance + mAdvertisingByte = aByte; + mObservers.iterate(workAdvertiseByte, this); + } + + // update all observers with a byte value + // build up a byte + // if requested, advertise the byte + void advertiseBit(bool aBit) { + // do the named thing first, of course + mAdvertisingBit = aBit; + mObservers.iterate(workAdvertiseBit, this); + } + + public: + ObservableDataStream() : mObservers() { + mAdvertisingBit = false; // we'll re-init on demand though + mAdvertisingByte = 0x07; // we'll re-init on demand though + } + + virtual ~ObservableDataStream() {} + + bool addObserver(String name, DataStreamObserver* obs) { return mObservers.add(name, obs); } + bool removeObserver(String name) { return mObservers.remove(name); } +}; + +inline bool DataStreamObserver::attach(ObservableDataStream* source) { return source->addObserver(observerName(), this); } + +inline bool DataStreamObserver::detach(ObservableDataStream* source) { return source->removeObserver(observerName()); } From 9520df8d9f6b32aa38848c90ae39da2a59d8ad42 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 5 Mar 2018 12:48:51 -0500 Subject: [PATCH 8/9] Add DeviceUsingBytes implementation --- CHANGELOG.md | 1 + README.md | 51 +++++++++- .../TestSomething/test/deviceusingbytes.cpp | 93 +++++++++++++++++++ cpp/arduino/HardwareSerial.h | 10 +- cpp/arduino/PinHistory.h | 17 ++-- cpp/arduino/ci/DeviceUsingBytes.h | 56 +++++++++++ 6 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 SampleProjects/TestSomething/test/deviceusingbytes.cpp create mode 100644 cpp/arduino/ci/DeviceUsingBytes.h diff --git a/CHANGELOG.md b/CHANGELOG.md index d42ca9a0..d372b446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Queue template implementation - Table template implementation - ObservableDataStream and DataStreamObserver pattern implementation +- DeviceUsingBytes and implementation of mocked serial device ### Changed - Unit test executables print to STDERR just in case there are segfaults. Uh, just in case I ever write any. diff --git a/README.md b/README.md index e494219e..70cbc68b 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ unittest(example_godmode_stuff) } ``` +#### Pin Histories + Of course, it's possible that your code might flip the bit more than once in a function. For that scenario, you may want to examine the history of a pin's commanded outputs: ```C++ @@ -102,6 +104,9 @@ unittest(pin_history) } ``` + +#### Pin Futures + Reading the pin more than once per function is also a possibility. In that case, we want to queue up a few values for the `digitalRead` or `analogRead` to find. ```C++ @@ -134,6 +139,9 @@ unittest(pin_read_history) } ``` +#### Serial Data + + A more complicated example: working with serial port IO. Let's say I have the following function: ```C++ @@ -191,10 +199,10 @@ unittest(two_flips) } ``` +#### Pin History as ASCII - -Finally, there are some cases where you want to use a pin as a serial port. There are history functions for that too. +For additional complexity, there are some cases where you want to use a pin as a serial port. There are history functions for that too. ```C++ int myPin = 3; @@ -217,6 +225,45 @@ Finally, there are some cases where you want to use a pin as a serial port. The assertEqual("Yes", state->digitalPin[myPin].toAscii(offset, bigEndian)); ``` +Instead of queueing bits as ASCII for future use with `toAscii`, you can send those bits directly (and immediately) to the output using `outgoingFromAscii`. Likewise, you can reinterpret/examine (as ASCII) the bits you have previously queued up by calling `incomingToAscii` on the PinHistory object. + + +#### Interactivity of "Devices" with Observers + +Even pin history and input/output buffers aren't capable of testing interactive code. For example, queueing the canned responses from a serial device before the requests are even sent to it is not a sane test environment; the library under test will see the entire future waiting for it on the input pin instead of a buffer that fills and empties over time. This calls for something more complicated. + +In this example, we create a simple class to emulate a Hayes modem. (For more information, dig into the `DataStreamObserver` code on which `DeviceUsingBytes` is based. + +```c++ +class FakeHayesModem : public DeviceUsingBytes { + public: + String mLast; + + FakeHayesModem() : DeviceUsingBytes() { + mLast = ""; + addResponseLine("AT", "OK"); + addResponseLine("ATV1", "NO CARRIER"); + } + virtual ~FakeHayesModem() {} + virtual void onMatchInput(String output) { mLast = output; } +}; + +unittest(modem_hardware) +{ + GodmodeState* state = GODMODE(); + state->reset(); + FakeHayesModem m; + m.attach(&Serial); + + Serial.write("AT\n"); + assertEqual("AT\n", state->serialPort[0].dataOut); + assertEqual("OK\n", m.mLast); +} +``` + +Note that instead of setting `mLast = output` in the `onMatchInput()` function for test purposes, we could just as easily queue some bytes to state->serialPort[0].dataIn for the library under test to find on its next `peek()` or `read()`. Or we could execute some action on a digital or analog input pin; the possibilities are fairly endless in this regard, although you will have to define them yourself -- from scratch -- extending the `DataStreamObserver` class to emulate your physical device. + + ## Overriding default build behavior You can add `.arduino-ci.yml` files to the project directory (which will then apply to both `test/` and `examples/`), as well as to the `test/` directory and each example directory in `examples/`. All defined fields can be overridden. diff --git a/SampleProjects/TestSomething/test/deviceusingbytes.cpp b/SampleProjects/TestSomething/test/deviceusingbytes.cpp new file mode 100644 index 00000000..7ae9a1b8 --- /dev/null +++ b/SampleProjects/TestSomething/test/deviceusingbytes.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include + +// DeviceUsingBytes extends DataStreamObserver, +// so we will be able to attach this class to an +// ObservableDataStream object, of which the pin +// history (soft-serial) and HardwareSerial +// objects are. +class FakeHayesModem : public DeviceUsingBytes { + public: + String mLast; + bool mMatchedInput; + + FakeHayesModem() : DeviceUsingBytes() { + mLast = ""; + mMatchedInput = false; + addResponseLine("AT", "OK"); + addResponseLine("ATV1", "NO CARRIER"); + } + + virtual ~FakeHayesModem() {} + + virtual void onMatchInput(String output) { + mLast = output; + mMatchedInput = true; + } +}; + +unittest(modem_hardware) +{ + GodmodeState* state = GODMODE(); + state->reset(); + + String cmd = "AT\n"; + + FakeHayesModem m; + m.attach(&Serial); + assertEqual(0, Serial.available()); + assertFalse(m.mMatchedInput); + assertEqual("", m.mMessage); + + for (int i = 0; i < cmd.length(); ++i) { + assertEqual(i, m.mMessage.length()); // before we write, length should equal i + Serial.write(cmd[i]); + } + assertEqual(0, m.mMessage.length()); // should have matched and reset + + assertEqual("", state->serialPort[0].dataIn); + assertEqual("AT\n", state->serialPort[0].dataOut); + + assureTrue(m.mMatchedInput); + //assertEqual(3, Serial.available()); + assertEqual("OK\n", m.mLast); +} + +unittest(modem_software) +{ + GodmodeState* state = GODMODE(); + state->reset(); + + bool bigEndian = false; + bool flipLogic = false; + SoftwareSerial ss(1, 2, flipLogic); + ss.listen(); + + String cmd = "AT\n"; + + FakeHayesModem m; + m.attach(&state->digitalPin[2]); + assertEqual(0, ss.available()); + assertFalse(m.mMatchedInput); + assertEqual("", m.mMessage); + + for (int i = 0; i < cmd.length(); ++i) { + assertEqual(i, m.mMessage.length()); // before we write, length should equal i + assertEqual(cmd.substr(0, i), state->digitalPin[2].toAscii(1, bigEndian)); + assertEqual(cmd.substr(0, i), m.mMessage); + ss.write(cmd[i]); + } + assertEqual(0, m.mMessage.length()); // should have matched and reset + + assertEqual("", state->digitalPin[1].incomingToAscii(1, bigEndian)); + assertEqual("AT\n", state->digitalPin[2].toAscii(1, bigEndian)); + + + assureTrue(m.mMatchedInput); + //assertEqual(3, Serial.available()); + assertEqual("OK\n", m.mLast); +} + +unittest_main() diff --git a/cpp/arduino/HardwareSerial.h b/cpp/arduino/HardwareSerial.h index af8701e7..758e304b 100644 --- a/cpp/arduino/HardwareSerial.h +++ b/cpp/arduino/HardwareSerial.h @@ -29,13 +29,13 @@ #define SERIAL_7O2 0x3C #define SERIAL_8O2 0x3E -class HardwareSerial : public Stream +class HardwareSerial : public Stream, public ObservableDataStream { protected: String* mGodmodeDataOut; public: - HardwareSerial(String* dataIn, String* dataOut, unsigned long* delay): Stream() { + HardwareSerial(String* dataIn, String* dataOut, unsigned long* delay): Stream(), ObservableDataStream() { mGodmodeDataIn = dataIn; mGodmodeDataOut = dataOut; mGodmodeMicrosDelay = delay; @@ -51,7 +51,11 @@ class HardwareSerial : public Stream // virtual int read(void); // virtual int availableForWrite(void); // virtual void flush(void); - virtual size_t write(uint8_t aChar) { mGodmodeDataOut->append(String((char)aChar)); return 1; } + virtual size_t write(uint8_t aChar) { + mGodmodeDataOut->append(String((char)aChar)); + advertiseByte((unsigned char)aChar); + return 1; + } inline size_t write(unsigned long n) { return write((uint8_t)n); } inline size_t write(long n) { return write((uint8_t)n); } diff --git a/cpp/arduino/PinHistory.h b/cpp/arduino/PinHistory.h index e4075ddb..85c56884 100644 --- a/cpp/arduino/PinHistory.h +++ b/cpp/arduino/PinHistory.h @@ -1,10 +1,11 @@ #pragma once #include "ci/Queue.h" +#include "ci/ObservableDataStream.h" #include "WString.h" // pins with history. template -class PinHistory { +class PinHistory : public ObservableDataStream { private: Queue qIn; Queue qOut; @@ -15,13 +16,14 @@ class PinHistory { } // enqueue ascii bits - void a2q(Queue &q, String input, bool bigEndian) { + void a2q(Queue &q, String input, bool bigEndian, bool advertise) { // 8 chars at a time, form up for (int j = 0; j < input.length(); ++j) { for (int i = 0; i < 8; ++i) { int shift = bigEndian ? 7 - i : i; unsigned char mask = (0x01 << shift); q.push(mask & input[j]); + if (advertise) advertiseBit(q.back()); // not valid for all possible types but whatever } } } @@ -61,7 +63,7 @@ class PinHistory { unsigned int asciiEncodingOffsetIn; unsigned int asciiEncodingOffsetOut; - PinHistory() { + PinHistory() : ObservableDataStream() { asciiEncodingOffsetIn = 0; // default is sensible asciiEncodingOffsetOut = 1; // default is sensible } @@ -87,6 +89,7 @@ class PinHistory { const T &operator=(const T& i) { qIn.clear(); qOut.push(i); + advertiseBit(qOut.back()); // not valid for all possible types but whatever return qOut.back(); } @@ -107,10 +110,11 @@ class PinHistory { for (int i = 0; i < length; ++i) qIn.push(arr[i]); } - // enqueue ascii bits - void fromAscii(String input, bool bigEndian) { a2q(qIn, input, bigEndian); } + // enqueue ascii bits for future use by the retrieve() function + void fromAscii(String input, bool bigEndian) { a2q(qIn, input, bigEndian, false); } - void outgoingFromAscii(String input, bool bigEndian) { a2q(qOut, input, bigEndian); } + // send a stream of ascii bits immediately + void outgoingFromAscii(String input, bool bigEndian) { a2q(qOut, input, bigEndian, true); } // convert the queue of incoming data to a string as if it was Serial comms // start from offset, consider endianness @@ -154,4 +158,3 @@ class PinHistory { } }; - diff --git a/cpp/arduino/ci/DeviceUsingBytes.h b/cpp/arduino/ci/DeviceUsingBytes.h new file mode 100644 index 00000000..639a733a --- /dev/null +++ b/cpp/arduino/ci/DeviceUsingBytes.h @@ -0,0 +1,56 @@ +#pragma once + +#include "ObservableDataStream.h" +#include "Table.h" +#include +#include + + +// Define a rudimentary serial device that responds to byte sequences +// +// The class monitors whatever stream it is observing, and builds up +// a buffer of the incoming data. If/when that data matches one of +// the stored responses, the buffer is cleared and the response to +// the matched requests is sent to the handler `onMatchInput` +// +// WARNING: if input is consumed and no matches are found, you are +// in a bad state where you can never match anything again. @TODO +// +// The extender of this abstract class should provide the following: +// 1. A set of responses using one of the provided convenience functions: +// * `addResponse`: request and response are taken verbatim +// * `addResponseLine`: request and response are appended a \n +// * `addResponseCRLF`: request and response are appended a \r\n +// 2. An action `onMatchInput` -- what to do with a response when triggered +class DeviceUsingBytes : public DataStreamObserver { + public: + String mMessage; + Table mResponses; + GodmodeState* state; + + + DeviceUsingBytes() : DataStreamObserver(true, false) { + mMessage = ""; + state = GODMODE(); + } + + virtual ~DeviceUsingBytes() {} + + bool addResponse(String hear, String say) { return mResponses.add(hear, say); } + bool addResponseLine(String hear, String say) { return mResponses.add(hear + "\n", say + "\n"); } + bool addResponseCRLF(String hear, String say) { return mResponses.add(hear + "\r\n", say + "\r\n"); } + + // what to do when there is a match + virtual void onMatchInput(String output) = 0; + + virtual String observerName() const { return "DeviceUsingBytes"; } + + virtual void onByte(unsigned char c) { + mMessage.concat(c); + if (mResponses.has(mMessage)) { + onMatchInput(mResponses.get(mMessage)); + mMessage = ""; + } + } +}; + From 06cb9d08bc6bd4507b332e5e60e259d90e48e784 Mon Sep 17 00:00:00 2001 From: Ian Katz Date: Mon, 5 Mar 2018 14:48:17 -0500 Subject: [PATCH 9/9] add templates --- .github/issue_template.md | 19 +++++++++++++++++++ .github/pull_request_template.md | 8 ++++++++ 2 files changed, 27 insertions(+) create mode 100644 .github/issue_template.md create mode 100644 .github/pull_request_template.md diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..5ada51e8 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,19 @@ +## System + + - OS: _(Travis/OSX/Linux/Windows)_ + - `ruby -v`: + - `bundle -v`: + - `bundle info arduino_ci`: + - `gcc -v`: + - Arduino IDE version: + - URL of failing Travis CI job: + - URL of your Arduino project: + + +## Issue / Feature Request Summary + + +## Arduino or Unit Test Code, Illustrating the Problem + + +## Arduino Architecture(s) Affected diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..ebdfed72 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ +## Highlights from `CHANGELOG.md` + +* See CHANGELOG.md for more + + +## Issues Fixed + +* Fixes #3000