Skip to content

Commit 709b697

Browse files
committed
Add support for streams
1 parent b0cb75a commit 709b697

File tree

6 files changed

+349
-0
lines changed

6 files changed

+349
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1414
- Many original Arduino `#define`s
1515
- Mocks for pinMode, analog/digital read/write
1616
- Support for WString
17+
- Support for Print
18+
- Support for Stream (backed by a String implementation)
1719

1820
### Changed
1921
- Made `wget` have quieter output
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#include <ArduinoUnitTests.h>
2+
#include <Arduino.h>
3+
4+
unittest(stream_construction)
5+
{
6+
String data = "";
7+
unsigned long micros = 100;
8+
9+
Stream s;
10+
s.mGodmodeDataIn = &data;
11+
s.mGodmodeMicrosDelay = &micros;
12+
13+
assertEqual(0, s.available());
14+
data = "abcd";
15+
assertEqual(4, s.available());
16+
assertEqual('a', s.peek());
17+
assertEqual('a', s.read());
18+
assertEqual("bcd", s.readString());
19+
assertEqual("", s.readString());
20+
21+
}
22+
23+
24+
unittest(stream_find)
25+
{
26+
String data = "";
27+
unsigned long micros = 100;
28+
29+
Stream s;
30+
s.mGodmodeDataIn = &data;
31+
s.mGodmodeMicrosDelay = &micros;
32+
33+
data = "abcdefghijkl";
34+
assertEqual('a', s.peek());
35+
assertEqual(true, s.find('f'));
36+
assertEqual('f', s.peek());
37+
assertEqual("fghijkl", s.readString());
38+
data = "fghijkl";
39+
assertEqual(false, s.findUntil("k", "j"));
40+
assertEqual('j', s.peek());
41+
}
42+
43+
unittest(stream_parse)
44+
{
45+
String data = "";
46+
unsigned long micros = 100;
47+
48+
Stream s;
49+
s.mGodmodeDataIn = &data;
50+
s.mGodmodeMicrosDelay = &micros;
51+
52+
long l;
53+
float f;
54+
data = "abcdefghijkl-123-456abcd";
55+
l = s.parseInt();
56+
assertEqual(-123, l);
57+
assertEqual('-', data[0]);
58+
l = s.parseInt();
59+
assertEqual(-456, l);
60+
l = s.parseInt();
61+
assertEqual(0, l);
62+
63+
data = "abc123.456-345.322";
64+
f = s.parseFloat();
65+
assertLess(123.456 - f, 0.0001);
66+
assertEqual('-', data[0]);
67+
f = s.parseFloat();
68+
assertLess(-345.322 - f, 0.0001);
69+
70+
}
71+
72+
unittest_main()

cpp/arduino/Arduino.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Where possible, variable names from the Arduino library are used to avoid confli
1212

1313
#include "WCharacter.h"
1414
#include "WString.h"
15+
#include "Print.h"
16+
#include "Stream.h"
1517

1618
typedef bool boolean;
1719
typedef uint8_t byte;

cpp/arduino/Print.h

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#pragma once
2+
3+
#include <stdio.h>
4+
#include "WString.h"
5+
6+
#define DEC 10
7+
#define HEX 16
8+
#define OCT 8
9+
#ifdef BIN
10+
#undef BIN
11+
#endif
12+
#define BIN 2
13+
14+
class Print;
15+
16+
class Printable
17+
{
18+
public:
19+
virtual size_t printTo(Print& p) const = 0;
20+
};
21+
22+
class Print
23+
{
24+
public:
25+
Print() {}
26+
27+
// Arduino's version of this is richer but until I see an actual error case I'm not sure how to mock
28+
int getWriteError() { return 0; }
29+
void clearWriteError() { }
30+
virtual int availableForWrite() { return 0; }
31+
32+
virtual size_t write(uint8_t) = 0;
33+
size_t write(const char *str) { return str == NULL ? 0 : write((const uint8_t *)str, strlen(str)); }
34+
virtual size_t write(const uint8_t *buffer, size_t size)
35+
{
36+
size_t n;
37+
for (n = 0; size && write(*buffer++) && ++n; --size);
38+
return n;
39+
}
40+
size_t write(const char *buffer, size_t size) { return write((const uint8_t *)buffer, size); }
41+
42+
size_t print(const String &s) { return write(s.c_str(), s.length()); }
43+
size_t print(const char* str) { return print(String(str)); }
44+
size_t print(char c) { return print(String(c)); }
45+
size_t print(unsigned char b, int base) { return print(String(b, base)); }
46+
size_t print(int n, int base) { return print(String(n, base)); }
47+
size_t print(unsigned int n, int base) { return print(String(n, base)); }
48+
size_t print(long n, int base) { return print(String(n, base)); }
49+
size_t print(unsigned long n, int base) { return print(String(n, base)); }
50+
size_t print(double n, int digits) { return print(String(n, digits)); }
51+
size_t print(const Printable& x) { return x.printTo(*this); }
52+
53+
size_t println(void) { return print("\r\n"); }
54+
size_t println(const String &s) { return print(s) + println(); }
55+
size_t println(const char* c) { return println(String(c)); }
56+
size_t println(char c) { return println(String(c)); }
57+
size_t println(unsigned char b, int base) { return println(String(b, base)); }
58+
size_t println(int num, int base) { return println(String(num, base)); }
59+
size_t println(unsigned int num, int base) { return println(String(num, base)); }
60+
size_t println(long num, int base) { return println(String(num, base)); }
61+
size_t println(unsigned long num, int base) { return println(String(num, base)); }
62+
size_t println(double num, int digits) { return println(String(num, digits)); }
63+
size_t println(const Printable& x) { return print(x) + println(); }
64+
65+
virtual void flush() { }
66+
67+
};

cpp/arduino/Stream.h

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#pragma once
2+
#include "WString.h"
3+
#include "Print.h"
4+
5+
6+
// This enumeration provides the lookahead options for parseInt(), parseFloat()
7+
// The rules set out here are used until either the first valid character is found
8+
// or a time out occurs due to lack of input.
9+
enum LookaheadMode{
10+
SKIP_ALL, // All invalid characters are ignored.
11+
SKIP_NONE, // Nothing is skipped, and the stream is not touched unless the first waiting character is valid.
12+
SKIP_WHITESPACE // Only tabs, spaces, line feeds & carriage returns are skipped.
13+
};
14+
15+
#define NO_IGNORE_CHAR '\x01' // a char not found in a valid ASCII numeric field
16+
17+
class Stream : public Print
18+
{
19+
public:
20+
String* mGodmodeDataIn;
21+
unsigned long* mGodmodeMicrosDelay;
22+
23+
protected:
24+
unsigned long mTimeoutMillis;
25+
26+
void fastforward(int pos) {
27+
mGodmodeDataIn->assign(mGodmodeDataIn->substring(pos));
28+
}
29+
30+
char fastforwardToAnyChar(LookaheadMode lookahead, String chars) {
31+
char c;
32+
while ((c = peek()) != -1) {
33+
if (chars.find(c) != String::npos) return c;
34+
if (lookahead == SKIP_NONE) return -1;
35+
if (lookahead == SKIP_WHITESPACE && !isWhitespace(c)) return -1;
36+
read();
37+
}
38+
return -1;
39+
}
40+
41+
// int timedRead(); // read stream with timeout
42+
// int timedPeek(); // peek stream with timeout
43+
// int peekNextDigit(LookaheadMode lookahead, bool detectDecimal); // returns the next numeric digit in the stream or -1 if timeout
44+
45+
public:
46+
virtual int available() { return mGodmodeDataIn->length(); }
47+
virtual int peek() { return available() ? (int)((*mGodmodeDataIn)[0]) : -1; }
48+
virtual int read() {
49+
int ret = peek();
50+
if (ret != -1) fastforward(1);
51+
return ret;
52+
}
53+
54+
virtual size_t write(uint8_t aChar) { mGodmodeDataIn->append(String((char)aChar)); return 1; }
55+
56+
57+
Stream() {
58+
mTimeoutMillis = 1000;
59+
mGodmodeMicrosDelay = NULL;
60+
mGodmodeDataIn = NULL;
61+
}
62+
63+
64+
void setTimeout(unsigned long timeoutMillis) { mTimeoutMillis = timeoutMillis; };
65+
unsigned long getTimeout(void) { return mTimeoutMillis; }
66+
67+
bool find(const String &s) {
68+
long idx;
69+
if ((idx = mGodmodeDataIn->find(s)) != String::npos) {
70+
fastforward(idx);
71+
return true;
72+
}
73+
return false;
74+
}
75+
76+
bool find(char *target) { return find(String(target)); }
77+
bool find(uint8_t *target) { return find(String((char*)target)); }
78+
bool find(char *target, size_t length) { return find(String(string(target, length))); }
79+
bool find(uint8_t *target, size_t length) { return find(String(string((char*)target, length))); }
80+
bool find(char target) { return find(String(string(&target, 1))); }
81+
82+
bool findUntil(const String &target, const String &terminator) {
83+
long idxTgt = mGodmodeDataIn->find(target);
84+
long idxTrm = mGodmodeDataIn->find(terminator);
85+
if (idxTgt == String::npos) {
86+
mGodmodeDataIn->clear();
87+
return false; // didn't find it
88+
}
89+
if (idxTrm != String::npos || idxTrm < idxTgt) {
90+
fastforward(idxTrm);
91+
return false; // target found after term
92+
}
93+
return find(target);
94+
}
95+
96+
bool findUntil(char *target, char *terminator) { return findUntil(String(target), String(terminator)); }
97+
bool findUntil(uint8_t *target, char *terminator) { return findUntil(String((char *)target), String(terminator)); }
98+
bool findUntil(char *target, size_t targetLen, char *terminate, size_t termLen) {
99+
return findUntil(String(string(target, targetLen)), String(string(terminate, termLen)));
100+
}
101+
bool findUntil(uint8_t *target, size_t targetLen, char *terminate, size_t termLen) {
102+
return findUntil(String(string((char *)target, targetLen)), String(string(terminate, termLen)));
103+
}
104+
105+
// returns the first valid (long) integer value from the current position.
106+
// lookahead determines how parseInt looks ahead in the stream.
107+
// See LookaheadMode enumeration at the top of the file.
108+
// Lookahead is terminated by the first character that is not a valid part of an integer.
109+
// Once parsing commences, 'ignore' will be skipped in the stream.
110+
long parseInt(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR) {
111+
String digits = "1234567890";
112+
if (fastforwardToAnyChar(lookahead, digits + "-") == -1) return 0;
113+
String out = String((char)read()); // read unconditionally -- might be a minus
114+
char c;
115+
bool keepGoing = true;
116+
do {
117+
c = peek();
118+
if (c == -1) break;
119+
if (c != ignore || ignore == NO_IGNORE_CHAR) out += c;
120+
keepGoing = digits.find(c) != String::npos;
121+
if (!keepGoing) break;
122+
read();
123+
} while (true);
124+
return out.toInt();
125+
}
126+
127+
float parseFloat(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR) {
128+
String digits = "1234567890";
129+
if (fastforwardToAnyChar(lookahead, digits + "-") == -1) return 0;
130+
String out = String((char)read()); // read unconditionally -- might be a minus
131+
String bank = digits + ".";
132+
bool gotDot = false;
133+
bool keepGoing = true;
134+
char c;
135+
do {
136+
c = peek();
137+
if (c == -1) break;
138+
if (c == '.') { // waiting for gotDot
139+
if (gotDot) break;
140+
gotDot = true;
141+
}
142+
if (c != ignore || ignore == NO_IGNORE_CHAR) out += c;
143+
keepGoing = bank.find(c) != String::npos;
144+
if (!keepGoing) break;
145+
read();
146+
} while (true);
147+
return out.toFloat();
148+
}
149+
150+
// read chars from stream into buffer
151+
// returns the number of characters placed in the buffer (0 means no valid data found)
152+
size_t readBytes(char *buffer, size_t length) {
153+
size_t ret = mGodmodeDataIn->copy(buffer, length);
154+
fastforward(ret);
155+
return ret;
156+
}
157+
158+
// read chars from stream into buffer
159+
// returns the number of characters placed in the buffer (0 means no valid data found)
160+
size_t readBytes(uint8_t *buffer, size_t length) { return readBytes((char *)buffer, length); }
161+
162+
// read chars from stream into buffer
163+
// returns the number of characters placed in the buffer (0 means no valid data found)
164+
size_t readBytesUntil(char terminator, char *buffer, size_t length) {
165+
size_t idx = mGodmodeDataIn->find(terminator);
166+
size_t howMuch = idx == String::npos ? length : min(length, idx);
167+
return readBytes(buffer, howMuch);
168+
}
169+
170+
// read chars from stream into buffer
171+
// returns the number of characters placed in the buffer (0 means no valid data found)
172+
size_t readBytesUntil(char terminator, uint8_t *buffer, size_t length) { return readBytesUntil(terminator, (char *)buffer, length); }
173+
174+
String readStringUntil(char terminator) {
175+
long idxTrm = mGodmodeDataIn->find(terminator);
176+
String ret;
177+
if (idxTrm == String::npos) {
178+
ret = String(*mGodmodeDataIn);
179+
mGodmodeDataIn->clear();
180+
} else {
181+
ret = mGodmodeDataIn->substring(0, idxTrm + 1);
182+
fastforward(idxTrm + 1);
183+
}
184+
return ret;
185+
}
186+
187+
String readString() {
188+
String ret(*mGodmodeDataIn);
189+
mGodmodeDataIn->clear();
190+
return ret;
191+
}
192+
193+
194+
protected:
195+
long parseInt(char ignore) { return parseInt(SKIP_ALL, ignore); }
196+
float parseFloat(char ignore) { return parseFloat(SKIP_ALL, ignore); }
197+
// These overload exists for compatibility with any class that has derived
198+
// Stream and used parseFloat/Int with a custom ignore character. To keep
199+
// the public API simple, these overload remains protected.
200+
201+
};
202+
203+
#undef NO_IGNORE_CHAR
204+
205+

spec/cpp_library_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"TestSomething/test/good-defines.cpp",
4141
"TestSomething/test/good-wcharacter.cpp",
4242
"TestSomething/test/good-wstring.cpp",
43+
"TestSomething/test/good-stream.cpp",
4344
"TestSomething/test/bad-null.cpp",
4445
]
4546
relative_paths = cpp_library.test_files.map { |f| f.split("SampleProjects/", 2)[1] }

0 commit comments

Comments
 (0)