diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c6279..d1531b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog * Unreleased +* 1.2.0 (2021-12-29) + * Simplify `StdioSerial` class, see + [Issue#43](https://github.com/bxparks/EpoxyDuino/issues/43). + * Replace input ring buffer with a buffer of one character. + * Wire `StdioSerial::write(uint8_t)` directly to Posix `write()`, + by-passing the `` buffer. `flush()` is no longer necessary. + * Thanks to @felias-fogg. + * **Revert Breaking Change Made in v1.1.0** Revert 432e304, so that + `Print::writeln()` writes `\r\n` again by default. + * Fixes [Issue#45](https://github.com/bxparks/EpoxyDuino/issues/45). + * Add `Print::setLineModeNormal()` and `Print::setLineModeUnix()` + methods to control the line termination behavior. + * See [README.md#UnixLineMode](README.md#UnixLineMode) for usage info. * 1.1.0 (2021-12-09) * Add optional `DEPS` variable containing header files that the `*.ino` depends on. diff --git a/README.md b/README.md index 8710213..e36f949 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The disadvantages are: environments (e.g. 16-bit `int` versus 32-bit `int`, or 32-bit `long` versus 64-bit `long`). -**Version**: 1.1.0 (2021-12-09) +**Version**: 1.2.0 (2021-12-09) **Changelog**: See [CHANGELOG.md](CHANGELOG.md) @@ -94,6 +94,7 @@ The disadvantages are: * [Supported Arduino Features](#SupportedArduinoFeatures) * [Arduino Functions](#ArduinoFunctions) * [Serial Port Emulation](#SerialPortEmulation) + * [Unix Line Mode](#UnixLineMode) * [Libraries and Mocks](#LibrariesAndMocks) * [Inherently Compatible Libraries](#InherentlyCompatibleLibraries) * [Emulation Libraries](#EmulationLibraries) @@ -729,6 +730,80 @@ the program. But this convenience means that the Arduino program running under `Serial.read()` function. The advantages of having normal Unix signals seemed worth the trade-off. + +### Unix Line Mode + +(Added in v1.2.0) + +The `Print` class in the Arduino API implements the `Print::println()` function +by printing the DOS line terminator characters `\r\n`. This decision make sense +when the serial port of the microcontroller is connected to a serial terminal, +which requires a `\r\n` at the end of each line to render the text properly. + +But when the Arduino application is executed on Linux machine, and the output is +redirected into a file, the `\r\n` is not consistent with the Unix convention +of using only a single `\n` to terminate each line. This causes the file to be +interpreted as a DOS-formatted file. Usually the DOS formatted file can be +processed without problems by other Linux programs and scripts, but sometimes +the extra `\r\n` causes problems, especially when mixed with a `Serial.printf()` +function using a single `\n`. + +EpoxyDuino provides a mechanism to configure the line termination convention for +a given application by providing 2 additional methods to its `Print` class: + +```C++ +class Print { + public: + // Use DOS line termination. This is the default. + void setLineModeNormal(); + + // Use Unix line termination. + void setLineModeUnix(); + + ... +}; +``` + +When an Arduino application is executed on a Linux machine using EpoxyDuino, +you can configure the `Serial` object in the `*.ino` file to use the Unix +convention like this: + +```C++ +void setup() { +#if ! defined(EPOXY_DUINO) + delay(1000); // wait to prevent garbage on Serial +#endif + + Serial.begin(115200); + while (!Serial); // Leonardo/Micro + +#if defined(EPOXY_DUINO) + Serial.setLineModeUnix(); +#endif +} +``` + +Why isn't `setLineModeUnix()` simply made to be the default on EpoxyDuino? +Because people write [AUnit](https://github.com/bxparks/AUnit) unit tests which +they expect will pass on both the microcontroller and on EpoxyDuino: + +```C++ +#include +#include +#include // PrintStr +... + +static void sayHello(Print& printer) { + printer.println("hello"); +} + +test(myTest) { + PrintStr<200> observed; + sayHello(observed); + assertEqual(observed.cstr(), "hello\r\n"); +} +``` + ## Libraries and Mocks diff --git a/cores/epoxy/Arduino.cpp b/cores/epoxy/Arduino.cpp index 26027de..1f196da 100644 --- a/cores/epoxy/Arduino.cpp +++ b/cores/epoxy/Arduino.cpp @@ -13,7 +13,7 @@ */ #include -#include // read(), STDIN_FILENO, usleep() +#include // usleep() #include // clock_gettime() #include "Arduino.h" @@ -22,11 +22,6 @@ // ----------------------------------------------------------------------- void yield() { - char c = '\0'; - if (read(STDIN_FILENO, &c, 1) == 1) { - Serial.insertChar(c); - } - usleep(1000); // prevents program from consuming 100% CPU } diff --git a/cores/epoxy/Arduino.h b/cores/epoxy/Arduino.h index 7a080bb..6efd1e7 100644 --- a/cores/epoxy/Arduino.h +++ b/cores/epoxy/Arduino.h @@ -14,8 +14,8 @@ #define EPOXY_DUINO_EPOXY_ARDUINO_H // xx.yy.zz => xxyyzz (without leading 0) -#define EPOXY_DUINO_VERSION 10100 -#define EPOXY_DUINO_VERSION_STRING "1.1.0" +#define EPOXY_DUINO_VERSION 10200 +#define EPOXY_DUINO_VERSION_STRING "1.2.0" #include // min(), max() #include // abs() diff --git a/cores/epoxy/Print.cpp b/cores/epoxy/Print.cpp index 30d6cb9..3d2bb20 100644 --- a/cores/epoxy/Print.cpp +++ b/cores/epoxy/Print.cpp @@ -129,7 +129,11 @@ size_t Print::print(const Printable& x) size_t Print::println(void) { - return write('\n'); + if (isLineModeUnix) { + return write('\n'); + } else { + return write("\r\n"); + } } size_t Print::println(const String &s) diff --git a/cores/epoxy/Print.h b/cores/epoxy/Print.h index fba28cb..935fba0 100644 --- a/cores/epoxy/Print.h +++ b/cores/epoxy/Print.h @@ -40,11 +40,27 @@ class Print { private: int write_error; + bool isLineModeUnix = false; + size_t printNumber(unsigned long, uint8_t); size_t printFloat(double, uint8_t); + protected: void setWriteError(int err = 1) { write_error = err; } + public: + /** + * Set the line termination mode to Normal (i.e. \\r\\n). This is the + * default. This function is available only on EpoxyDuino. + */ + void setLineModeNormal() { isLineModeUnix = false; } + + /** + * Set the line termination mode to Unix (i.e. \\n). This function is + * available only on EpoxyDuino. + */ + void setLineModeUnix() { isLineModeUnix = true; } + Print() : write_error(0) {} int getWriteError() { return write_error; } diff --git a/cores/epoxy/StdioSerial.cpp b/cores/epoxy/StdioSerial.cpp index b1b985c..b378f0f 100644 --- a/cores/epoxy/StdioSerial.cpp +++ b/cores/epoxy/StdioSerial.cpp @@ -4,15 +4,33 @@ */ #include +#include #include "StdioSerial.h" size_t StdioSerial::write(uint8_t c) { - int result = putchar(c); - return (result == EOF) ? 0 : 1; + ssize_t status = ::write(STDOUT_FILENO, &c, 1); + return (status <= 0) ? 0 : 1; } -void StdioSerial::flush() { - fflush(stdout); +int StdioSerial::read() { + int ch = peek(); + bufch = -1; + return ch; +} + +int StdioSerial::peek() { + if (bufch == -1) { + // 'c' must be unsigned to avoid ambiguity with -1 in-band error condition + unsigned char c; + ssize_t status = ::read(STDIN_FILENO, &c, 1); + bufch = (status <= 0) ? -1 : c; + } + return bufch; +} + +int StdioSerial::available() { + int ch = peek(); + return (int) (ch != -1); } StdioSerial Serial; diff --git a/cores/epoxy/StdioSerial.h b/cores/epoxy/StdioSerial.h index 443637a..74b9a5a 100644 --- a/cores/epoxy/StdioSerial.h +++ b/cores/epoxy/StdioSerial.h @@ -15,50 +15,20 @@ */ class StdioSerial: public Stream { public: - void begin(unsigned long /*baud*/) { } + void begin(unsigned long /*baud*/) { bufch = -1; } size_t write(uint8_t c) override; - void flush() override; - operator bool() { return true; } - int available() override { return mHead != mTail; } - - int read() override { - if (mHead == mTail) { - return -1; - } else { - char c = mBuffer[mHead]; - mHead = (mHead + 1) % kBufSize; - return c; - } - } - - int peek() override { - return (mHead != mTail) ? mBuffer[mHead] : -1; - } - - /** Insert a character into the ring buffer. */ - void insertChar(char c) { - int newTail = (mTail + 1) % kBufSize; - if (newTail == mHead) { - // Buffer full, drop the character. (Strictly speaking, there's one - // remaining slot in the buffer, but we can't use it because we need to - // distinguish between buffer-empty and buffer-full). - return; - } - mBuffer[mTail] = c; - mTail = newTail; - } + int available() override; + int read() override; + + int peek() override; + private: - // Ring buffer size (should be a power of 2 for efficiency). - static const int kBufSize = 128; - - char mBuffer[kBufSize]; - int mHead = 0; - int mTail = 0; + int bufch; }; extern StdioSerial Serial; diff --git a/cores/epoxy/main.cpp b/cores/epoxy/main.cpp index 6c9cdc8..ecdce55 100644 --- a/cores/epoxy/main.cpp +++ b/cores/epoxy/main.cpp @@ -87,7 +87,7 @@ static void enableRawMode() { raw.c_cflag |= (CS8); // Enable ISIG to allow Ctrl-C to kill the program. - raw.c_lflag &= ~(/*ECHO | ISIG |*/ ICANON | IEXTEN); + raw.c_lflag &= ~(ECHO | /*ISIG |*/ ICANON | IEXTEN); raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { diff --git a/examples/echo/Makefile b/examples/echo/Makefile new file mode 100644 index 0000000..6def24b --- /dev/null +++ b/examples/echo/Makefile @@ -0,0 +1,6 @@ +# See https://github.com/bxparks/EpoxyDuino for documentation about this +# Makefile to compile and run Arduino programs natively on Linux or MacOS. + +APP_NAME := echo +ARDUINO_LIBS := +include ../../../EpoxyDuino/EpoxyDuino.mk diff --git a/examples/echo/echo.ino b/examples/echo/echo.ino new file mode 100644 index 0000000..f577019 --- /dev/null +++ b/examples/echo/echo.ino @@ -0,0 +1,107 @@ +/* + * Test reading and writing from stdin and stdout, using the keyboard or pipes. + * + * Usage: + * + * To test keyboard input: + * $ ./echo.out + * Echo test + * 'char' is signed. + * # Type 'a', 'b', 'c, 'd' on keyboad + * 61('a')62('b')63('c')64('d') + * + * To test reading of 0xFF character (which should not interfere with -1 error): + * $ printf '\xff' | ./echo.out + * Echo test + * 'char' is signed. + * FF(' ') + * + * To test reading from a directory, which generates a -1 error status when + * ::read() is called: + * $ ./echo.out < . + * Echo test + * 'char' is signed. + * # Nothing should print. + * + * To test piping: + * $ yes | ./echo.out + * Echo test + * 'char' is signed. + * 79('y')0A(' ')79('y')0A(' ')[...] + */ + +#include + +/** + * Print the hexadecimal value of the character. If the character is printable + * in ASCII, print its character inside parenthesis. Print a space for + * non-printable character. Example, "61('a')" if the 'a' is passed as the + * argument. This is useful for determining if an 0xFF byte can be read from the + * Serial port without interfering with the -1 error indicator. + */ +void debugPrint(int c) { + if (c < 0) { + // This should never happen, but print the value of c if it does. + Serial.print(c); + } else { + // Print 2 digit hex padded with 0. + if (c < 16) { + Serial.print('0'); + } + Serial.print(c, 16); + } + + // Print printable character, or space for non-printable. + Serial.print("('"); + Serial.print((c > 32 && c < 127) ? (char) c : ' '); + Serial.print("')"); +} + +/** Read the Serial port using an explicit while-loop. */ +void loopExplicitly() { + while (true) { + if (Serial.available()) { + int c = Serial.read(); + debugPrint(c); + } + } +} + +/** + * Read the Serial port using the implicit Arduino loop(), with a non-blocking + * one second delay to test the buffering of the keyboard input. + */ +void loopImplicitly() { + static uint16_t prevMillis = 0; + + uint16_t nowMillis = millis(); + if ((uint16_t) (nowMillis - prevMillis) > 1000) { + prevMillis = nowMillis; + if (Serial.available()) { + int c = Serial.read(); + debugPrint(c); + } + } +} + +//----------------------------------------------------------------------------- + +void setup(void) { + delay(1000); + Serial.begin(115200); + while (!Serial); + + // Check if 'char' is a signed or unsigned on this system. + Serial.println(F("Echo test")); + char c = (char) 128; + int i = c; + if (i < 0) { + Serial.println(F("'char' is signed.")); + } else { + Serial.println(F("'char' is unsigned.")); + } +} + +void loop(void) { + loopExplicitly(); +} diff --git a/library.json b/library.json index eff297a..5145f04 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "EpoxyDuino", - "version": "1.1.0", + "version": "1.2.0", "description": "Compile and run Arduino programs natively on Linux, MacOS and FreeBSD.", "keywords": [ "unit-test",