Skip to content

Commit 9520df8

Browse files
committed
Add DeviceUsingBytes implementation
1 parent 80f4f6b commit 9520df8

File tree

6 files changed

+216
-12
lines changed

6 files changed

+216
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1515
- Queue template implementation
1616
- Table template implementation
1717
- ObservableDataStream and DataStreamObserver pattern implementation
18+
- DeviceUsingBytes and implementation of mocked serial device
1819

1920
### Changed
2021
- Unit test executables print to STDERR just in case there are segfaults. Uh, just in case I ever write any.

README.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ unittest(example_godmode_stuff)
7070
}
7171
```
7272

73+
#### Pin Histories
74+
7375
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:
7476

7577
```C++
@@ -102,6 +104,9 @@ unittest(pin_history)
102104
}
103105
```
104106

107+
108+
#### Pin Futures
109+
105110
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.
106111

107112
```C++
@@ -134,6 +139,9 @@ unittest(pin_read_history)
134139
}
135140
```
136141

142+
#### Serial Data
143+
144+
137145
A more complicated example: working with serial port IO. Let's say I have the following function:
138146

139147
```C++
@@ -191,10 +199,10 @@ unittest(two_flips)
191199
}
192200
```
193201

202+
#### Pin History as ASCII
194203

195204

196-
197-
Finally, there are some cases where you want to use a pin as a serial port. There are history functions for that too.
205+
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.
198206

199207
```C++
200208
int myPin = 3;
@@ -217,6 +225,45 @@ Finally, there are some cases where you want to use a pin as a serial port. The
217225
assertEqual("Yes", state->digitalPin[myPin].toAscii(offset, bigEndian));
218226
```
219227

228+
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.
229+
230+
231+
#### Interactivity of "Devices" with Observers
232+
233+
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.
234+
235+
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.
236+
237+
```c++
238+
class FakeHayesModem : public DeviceUsingBytes {
239+
public:
240+
String mLast;
241+
242+
FakeHayesModem() : DeviceUsingBytes() {
243+
mLast = "";
244+
addResponseLine("AT", "OK");
245+
addResponseLine("ATV1", "NO CARRIER");
246+
}
247+
virtual ~FakeHayesModem() {}
248+
virtual void onMatchInput(String output) { mLast = output; }
249+
};
250+
251+
unittest(modem_hardware)
252+
{
253+
GodmodeState* state = GODMODE();
254+
state->reset();
255+
FakeHayesModem m;
256+
m.attach(&Serial);
257+
258+
Serial.write("AT\n");
259+
assertEqual("AT\n", state->serialPort[0].dataOut);
260+
assertEqual("OK\n", m.mLast);
261+
}
262+
```
263+
264+
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.
265+
266+
220267
## Overriding default build behavior
221268

222269
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.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#include <Arduino.h>
2+
#include <ArduinoUnitTests.h>
3+
#include <SoftwareSerial.h>
4+
#include <ci/DeviceUsingBytes.h>
5+
6+
// DeviceUsingBytes extends DataStreamObserver,
7+
// so we will be able to attach this class to an
8+
// ObservableDataStream object, of which the pin
9+
// history (soft-serial) and HardwareSerial
10+
// objects are.
11+
class FakeHayesModem : public DeviceUsingBytes {
12+
public:
13+
String mLast;
14+
bool mMatchedInput;
15+
16+
FakeHayesModem() : DeviceUsingBytes() {
17+
mLast = "";
18+
mMatchedInput = false;
19+
addResponseLine("AT", "OK");
20+
addResponseLine("ATV1", "NO CARRIER");
21+
}
22+
23+
virtual ~FakeHayesModem() {}
24+
25+
virtual void onMatchInput(String output) {
26+
mLast = output;
27+
mMatchedInput = true;
28+
}
29+
};
30+
31+
unittest(modem_hardware)
32+
{
33+
GodmodeState* state = GODMODE();
34+
state->reset();
35+
36+
String cmd = "AT\n";
37+
38+
FakeHayesModem m;
39+
m.attach(&Serial);
40+
assertEqual(0, Serial.available());
41+
assertFalse(m.mMatchedInput);
42+
assertEqual("", m.mMessage);
43+
44+
for (int i = 0; i < cmd.length(); ++i) {
45+
assertEqual(i, m.mMessage.length()); // before we write, length should equal i
46+
Serial.write(cmd[i]);
47+
}
48+
assertEqual(0, m.mMessage.length()); // should have matched and reset
49+
50+
assertEqual("", state->serialPort[0].dataIn);
51+
assertEqual("AT\n", state->serialPort[0].dataOut);
52+
53+
assureTrue(m.mMatchedInput);
54+
//assertEqual(3, Serial.available());
55+
assertEqual("OK\n", m.mLast);
56+
}
57+
58+
unittest(modem_software)
59+
{
60+
GodmodeState* state = GODMODE();
61+
state->reset();
62+
63+
bool bigEndian = false;
64+
bool flipLogic = false;
65+
SoftwareSerial ss(1, 2, flipLogic);
66+
ss.listen();
67+
68+
String cmd = "AT\n";
69+
70+
FakeHayesModem m;
71+
m.attach(&state->digitalPin[2]);
72+
assertEqual(0, ss.available());
73+
assertFalse(m.mMatchedInput);
74+
assertEqual("", m.mMessage);
75+
76+
for (int i = 0; i < cmd.length(); ++i) {
77+
assertEqual(i, m.mMessage.length()); // before we write, length should equal i
78+
assertEqual(cmd.substr(0, i), state->digitalPin[2].toAscii(1, bigEndian));
79+
assertEqual(cmd.substr(0, i), m.mMessage);
80+
ss.write(cmd[i]);
81+
}
82+
assertEqual(0, m.mMessage.length()); // should have matched and reset
83+
84+
assertEqual("", state->digitalPin[1].incomingToAscii(1, bigEndian));
85+
assertEqual("AT\n", state->digitalPin[2].toAscii(1, bigEndian));
86+
87+
88+
assureTrue(m.mMatchedInput);
89+
//assertEqual(3, Serial.available());
90+
assertEqual("OK\n", m.mLast);
91+
}
92+
93+
unittest_main()

cpp/arduino/HardwareSerial.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@
2929
#define SERIAL_7O2 0x3C
3030
#define SERIAL_8O2 0x3E
3131

32-
class HardwareSerial : public Stream
32+
class HardwareSerial : public Stream, public ObservableDataStream
3333
{
3434
protected:
3535
String* mGodmodeDataOut;
3636

3737
public:
38-
HardwareSerial(String* dataIn, String* dataOut, unsigned long* delay): Stream() {
38+
HardwareSerial(String* dataIn, String* dataOut, unsigned long* delay): Stream(), ObservableDataStream() {
3939
mGodmodeDataIn = dataIn;
4040
mGodmodeDataOut = dataOut;
4141
mGodmodeMicrosDelay = delay;
@@ -51,7 +51,11 @@ class HardwareSerial : public Stream
5151
// virtual int read(void);
5252
// virtual int availableForWrite(void);
5353
// virtual void flush(void);
54-
virtual size_t write(uint8_t aChar) { mGodmodeDataOut->append(String((char)aChar)); return 1; }
54+
virtual size_t write(uint8_t aChar) {
55+
mGodmodeDataOut->append(String((char)aChar));
56+
advertiseByte((unsigned char)aChar);
57+
return 1;
58+
}
5559

5660
inline size_t write(unsigned long n) { return write((uint8_t)n); }
5761
inline size_t write(long n) { return write((uint8_t)n); }

cpp/arduino/PinHistory.h

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
#pragma once
22
#include "ci/Queue.h"
3+
#include "ci/ObservableDataStream.h"
34
#include "WString.h"
45

56
// pins with history.
67
template <typename T>
7-
class PinHistory {
8+
class PinHistory : public ObservableDataStream {
89
private:
910
Queue<T> qIn;
1011
Queue<T> qOut;
@@ -15,13 +16,14 @@ class PinHistory {
1516
}
1617

1718
// enqueue ascii bits
18-
void a2q(Queue<T> &q, String input, bool bigEndian) {
19+
void a2q(Queue<T> &q, String input, bool bigEndian, bool advertise) {
1920
// 8 chars at a time, form up
2021
for (int j = 0; j < input.length(); ++j) {
2122
for (int i = 0; i < 8; ++i) {
2223
int shift = bigEndian ? 7 - i : i;
2324
unsigned char mask = (0x01 << shift);
2425
q.push(mask & input[j]);
26+
if (advertise) advertiseBit(q.back()); // not valid for all possible types but whatever
2527
}
2628
}
2729
}
@@ -61,7 +63,7 @@ class PinHistory {
6163
unsigned int asciiEncodingOffsetIn;
6264
unsigned int asciiEncodingOffsetOut;
6365

64-
PinHistory() {
66+
PinHistory() : ObservableDataStream() {
6567
asciiEncodingOffsetIn = 0; // default is sensible
6668
asciiEncodingOffsetOut = 1; // default is sensible
6769
}
@@ -87,6 +89,7 @@ class PinHistory {
8789
const T &operator=(const T& i) {
8890
qIn.clear();
8991
qOut.push(i);
92+
advertiseBit(qOut.back()); // not valid for all possible types but whatever
9093
return qOut.back();
9194
}
9295

@@ -107,10 +110,11 @@ class PinHistory {
107110
for (int i = 0; i < length; ++i) qIn.push(arr[i]);
108111
}
109112

110-
// enqueue ascii bits
111-
void fromAscii(String input, bool bigEndian) { a2q(qIn, input, bigEndian); }
113+
// enqueue ascii bits for future use by the retrieve() function
114+
void fromAscii(String input, bool bigEndian) { a2q(qIn, input, bigEndian, false); }
112115

113-
void outgoingFromAscii(String input, bool bigEndian) { a2q(qOut, input, bigEndian); }
116+
// send a stream of ascii bits immediately
117+
void outgoingFromAscii(String input, bool bigEndian) { a2q(qOut, input, bigEndian, true); }
114118

115119
// convert the queue of incoming data to a string as if it was Serial comms
116120
// start from offset, consider endianness
@@ -154,4 +158,3 @@ class PinHistory {
154158
}
155159

156160
};
157-

cpp/arduino/ci/DeviceUsingBytes.h

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#pragma once
2+
3+
#include "ObservableDataStream.h"
4+
#include "Table.h"
5+
#include <WString.h>
6+
#include <Godmode.h>
7+
8+
9+
// Define a rudimentary serial device that responds to byte sequences
10+
//
11+
// The class monitors whatever stream it is observing, and builds up
12+
// a buffer of the incoming data. If/when that data matches one of
13+
// the stored responses, the buffer is cleared and the response to
14+
// the matched requests is sent to the handler `onMatchInput`
15+
//
16+
// WARNING: if input is consumed and no matches are found, you are
17+
// in a bad state where you can never match anything again. @TODO
18+
//
19+
// The extender of this abstract class should provide the following:
20+
// 1. A set of responses using one of the provided convenience functions:
21+
// * `addResponse`: request and response are taken verbatim
22+
// * `addResponseLine`: request and response are appended a \n
23+
// * `addResponseCRLF`: request and response are appended a \r\n
24+
// 2. An action `onMatchInput` -- what to do with a response when triggered
25+
class DeviceUsingBytes : public DataStreamObserver {
26+
public:
27+
String mMessage;
28+
Table<String, String> mResponses;
29+
GodmodeState* state;
30+
31+
32+
DeviceUsingBytes() : DataStreamObserver(true, false) {
33+
mMessage = "";
34+
state = GODMODE();
35+
}
36+
37+
virtual ~DeviceUsingBytes() {}
38+
39+
bool addResponse(String hear, String say) { return mResponses.add(hear, say); }
40+
bool addResponseLine(String hear, String say) { return mResponses.add(hear + "\n", say + "\n"); }
41+
bool addResponseCRLF(String hear, String say) { return mResponses.add(hear + "\r\n", say + "\r\n"); }
42+
43+
// what to do when there is a match
44+
virtual void onMatchInput(String output) = 0;
45+
46+
virtual String observerName() const { return "DeviceUsingBytes"; }
47+
48+
virtual void onByte(unsigned char c) {
49+
mMessage.concat(c);
50+
if (mResponses.has(mMessage)) {
51+
onMatchInput(mResponses.get(mMessage));
52+
mMessage = "";
53+
}
54+
}
55+
};
56+

0 commit comments

Comments
 (0)