diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bae30ec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI + +on: + push: + pull_request: + +jobs: + format: + name: clang-format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Check formatting + uses: jidicula/clang-format-action@v4.13.0 + with: + clang-format-version: "17" + check-path: "." + + lint: + name: arduino-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Lint library + uses: arduino/arduino-lint-action@v3 + with: + # Use "submit" until the library is accepted into the Library Manager + # index; switch to "update" once it is listed. + library-manager: submit + compliance: strict + + compile: + name: compile examples + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + # Each board carries the platform(s) the FQBN needs. AVR ships with + # the Arduino CLI; the ESP cores are pulled from their package indexes. + - fqbn: arduino:avr:uno + platforms: | + - name: arduino:avr + - fqbn: arduino:avr:mega + platforms: | + - name: arduino:avr + - fqbn: esp8266:esp8266:nodemcuv2 + platforms: | + - name: esp8266:esp8266 + source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json + - fqbn: esp32:esp32:esp32 + platforms: | + - name: esp32:esp32 + source-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + - fqbn: rp2040:rp2040:rpipico + platforms: | + - name: rp2040:rp2040 + source-url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json + steps: + - uses: actions/checkout@v7 + - name: Compile example sketches + uses: arduino/compile-sketches@v1 + with: + fqbn: ${{ matrix.board.fqbn }} + platforms: ${{ matrix.board.platforms }} + libraries: | + - source-path: ./ + - source-url: https://github.com/madleech/Auto485.git + sketch-paths: | + - examples/echo + - examples/periodic_transmit_1 + - examples/periodic_transmit_2 + + test: + name: native unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.x" + - name: Install PlatformIO + run: pip install --upgrade platformio + - name: Run tests + run: pio test -e native diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8dba61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Mac crap +.DS_Store + + +############# +## PlatformIO +############# + +.pio/ +.pioenvs/ +.piolibdeps/ +.vscode/ +*.o +*.a diff --git a/README.md b/README.md index 85a2db3..13856dc 100644 --- a/README.md +++ b/README.md @@ -58,32 +58,23 @@ In our main loop we check if any input is available, and if it is, we set the bu Documentation ------------- -**Auto485(int DE_pin)** -Creates a new Auto485 object. The Driver and Receiver Enable pins on the MAX485 are connected together. +`Auto485(int DE_pin)` – Creates a new Auto485 object. The Driver and Receiver Enable pins on the MAX485 are connected together. -**Auto485(int DE_pin, int RE_pin)** -Creates a new Auto485 object. The Driver and Receiver Enable pins on the MAX485 are connected to separate pins on the Arduino. +`Auto485(int DE_pin, int RE_pin)` – Creates a new Auto485 object. The Driver and Receiver Enable pins on the MAX485 are connected to separate pins on the Arduino. -**Auto485(int DE_pin, int RE_pin, HardwareSerial serial_port)** -Creates a new Auto485 object using the specified serial port. Usually the defaults are fine (Serial on most boards, Serial1 on the Leonardo and other USBCOM boards). If you are using a Mega with multiple serial ports this lets you choose which serial port to use. +`Auto485(int DE_pin, int RE_pin, HardwareSerial serial_port)` – Creates a new Auto485 object using the specified serial port. Usually the defaults are fine (Serial on most boards, Serial1 on the Leonardo and other USBCOM boards). If you are using a Mega with multiple serial ports this lets you choose which serial port to use. -**Auto485::TX**, **Auto485::RX** -These are constants to let you toggle the mode of the bus. +`Auto485::TX`, `Auto485::RX` – These are constants to let you toggle the mode of the bus. -**begin(baud), begin(baud, config)** -Initiate a serial connection at the given speed, and optionally with the given config settings (e.g. `SERIAL_8N2` to use two stop bits, etc). +`begin(baud), begin(baud, config)` – Initiate a serial connection at the given speed, and optionally with the given config settings (e.g. `SERIAL_8N2` to use two stop bits, etc). -**set_mode(Auto485::TX)**, **set_mode(Auto485::RX)** -Manually change to transmit or receive mode. When returning to receive mode, the function will pause until all pending serial data has been sent. +`set_mode(Auto485::TX)`, `set_mode(Auto485::RX)` – Manually change to transmit or receive mode. When returning to receive mode, the function will pause until all pending serial data has been sent. -**write(...)**, **print(...)** -When in receive mode, the first call to any output functions will change to transmit mode, then send out the data as expected. It handles all the formatting options of the regular Arduino `print` and `write` functions. +`write(...)`, `print(...)` – When in receive mode, the first call to any output functions will change to transmit mode, then send out the data as expected. It handles all the formatting options of the regular Arduino `print` and `write` functions. -**flush()** -Finish writing data, then switch to receive mode. Usually the serial writing functions happen asynchronously, with no delay while the data is sent out the serial port. When we're operating in half duplex mode though, we need to wait for the data to finish being sent before we change the mode of the bus. By calling `.flush()` we ensure there is no unsent data in the buffer. Once all pending data has been sent, we automatically switch back to receive mode! +`flush()` – Finish writing data, then switch to receive mode. Usually the serial writing functions happen asynchronously, with no delay while the data is sent out the serial port. When we're operating in half duplex mode though, we need to wait for the data to finish being sent before we change the mode of the bus. By calling `.flush()` we ensure there is no unsent data in the buffer. Once all pending data has been sent, we automatically switch back to receive mode! -**println(...)** -Like the `write(...)` and `print(...)` functions, calling `println(...)` will automatically switch to transmit mode. Unlike the lower-level functions though, println will return to receive mode at the end of the line. This means you can easily print simple messages to the bus and everything operates as expected, but if you're sending data byte-by-byte, that functionality if there too and Auto485 won't toggle between RX and TX for every single byte you send. +`println(...)` – Like the `write(...)` and `print(...)` functions, calling `println(...)` will automatically switch to transmit mode. Unlike the lower-level functions though, println will return to receive mode at the end of the line. This means you can easily print simple messages to the bus and everything operates as expected, but if you're sending data byte-by-byte, that functionality if there too and Auto485 won't toggle between RX and TX for every single byte you send. License ------- diff --git a/examples/echo/echo.ino b/examples/echo/echo.ino index 84b5909..9bbad6d 100644 --- a/examples/echo/echo.ino +++ b/examples/echo/echo.ino @@ -2,7 +2,7 @@ /* * Listens for data from PC and echos it back. - * + * * Uses both the "auto" and "manual" modes of Auto485. */ @@ -11,16 +11,19 @@ Auto485 bus(DE_PIN, RE_PIN); // new Auto485 wrapper using DE_PIN & RE_PIN to toggle read/write mode on the MAX485 -void setup() { - bus.begin(9600); // open bus at 9600bps - bus.println("Hello world, now listening"); // at the end of println, we return to listening +void setup() +{ + bus.begin(9600); // open bus at 9600bps + bus.println("Hello world, now listening"); // at the end of println, we return to listening } -void loop() { - if (bus.available() > 0) { - while (bus.available() > 0) - bus.write(bus.read()); // mode -> transmit - bus.println(); // mode -> receive - } - delay(1000); +void loop() +{ + if (bus.available() > 0) + { + while (bus.available() > 0) + bus.write(bus.read()); // mode -> transmit + bus.println(); // mode -> receive + } + delay(1000); } diff --git a/examples/periodic_transmit_1/periodic_transmit_1.ino b/examples/periodic_transmit_1/periodic_transmit_1.ino index 951c641..34a2120 100644 --- a/examples/periodic_transmit_1/periodic_transmit_1.ino +++ b/examples/periodic_transmit_1/periodic_transmit_1.ino @@ -3,7 +3,7 @@ /* * Periodically writes some characters to the hardware serial port, * which should be hooked up to the MAX485. - * + * * Uses the "transparent" mode of Auto485, which automatically toggles between TX and RX mode. */ @@ -12,19 +12,22 @@ Auto485 bus(DE_PIN, RE_PIN); // new Auto485 wrapper using DE_PIN & RE_PIN to toggle read/write mode on the MAX485 -void setup() { - bus.begin(9600); // start talking at 9600bps +void setup() +{ + bus.begin(9600); // start talking at 9600bps } int i = 0; char x[4] = {'A', 'B', 'C', 'D'}; -void loop() { - tx(x[i++ % 4]); - delay(500); +void loop() +{ + tx(x[i++ % 4]); + delay(500); } -void tx(char c) { - bus.write(c); - bus.flush(); // since hardware serial is asynchronous. This ends the transmit cycle and returns to receive mode +void tx(char c) +{ + bus.write(c); + bus.flush(); // since hardware serial is asynchronous. This ends the transmit cycle and returns to receive mode } diff --git a/examples/periodic_transmit_2/periodic_transmit_2.ino b/examples/periodic_transmit_2/periodic_transmit_2.ino index c5ad913..faf7865 100644 --- a/examples/periodic_transmit_2/periodic_transmit_2.ino +++ b/examples/periodic_transmit_2/periodic_transmit_2.ino @@ -3,7 +3,7 @@ /* * Periodically writes some characters to the hardware serial port, * which should be hooked up to the MAX485. - * + * * Uses the "manual" mode of Auto485, which lets you control TX and RX mode. */ @@ -12,20 +12,23 @@ Auto485 bus(DE_PIN, RE_PIN); // new Auto485 wrapper using DE_PIN & RE_PIN to toggle read/write mode on the MAX485 -void setup() { - bus.begin(9600); // start talking at 9600bps +void setup() +{ + bus.begin(9600); // start talking at 9600bps } int i = 0; char x[4] = {'A', 'B', 'C', 'D'}; -void loop() { - tx(x[i++ % 4]); - delay(500); +void loop() +{ + tx(x[i++ % 4]); + delay(500); } -void tx(char c) { - bus.set_mode(Auto485::TX); // mode = transmit - bus.write(c); - bus.set_mode(Auto485::RX); // mode = receive, will pause until all pending serial data has been transmitted +void tx(char c) +{ + bus.set_mode(Auto485::TX); // mode = transmit + bus.write(c); + bus.set_mode(Auto485::RX); // mode = receive, will pause until all pending serial data has been transmitted } diff --git a/extras/hardware_test/README.md b/extras/hardware_test/README.md new file mode 100644 index 0000000..cfd255c --- /dev/null +++ b/extras/hardware_test/README.md @@ -0,0 +1,24 @@ +# Test on Real Hardware + +This is a tiny sketch to test the current library code on a real device. It +drives a half-duplex RS485 transceiver over the hardware Serial port: once a +second it transmits a heartbeat line, and it echoes any received bytes back onto +the bus. Both directions rely on Auto485 flipping the DE/RE pins automatically. + +Wiring: Arduino pin 2 -> MAX485 DE and RE (tied together); Serial TX/RX -> the +MAX485 DI/RO. + +Build + upload + monitor: +``` +$ pio run -d extras/hardware_test -e uno -t upload +$ pio device monitor -b 9600 +``` + +Meaningfully exercising the bus needs a second RS485 node (or a USB RS485 +adapter) to talk to. Without one, this still serves as a compile/upload/boot +smoke test against the local library source. +``` +$ pio run -d extras/hardware_test -e uno +... +SUCCESS +``` diff --git a/extras/hardware_test/platformio.ini b/extras/hardware_test/platformio.ini new file mode 100644 index 0000000..c4d8f04 --- /dev/null +++ b/extras/hardware_test/platformio.ini @@ -0,0 +1,27 @@ +; Hardware smoke-test for the Auto485 library. +; +; Builds the demo sketch in src/main.cpp against the LOCAL library source (this +; working tree - i.e. your unreleased changes), not a published release, and +; uploads it to a real board. +; +; lib_extra_dirs points the Library Dependency Finder at the directory that +; CONTAINS this library (the parent folder), so PlatformIO reads the source in +; place - no copying and no symlinks. Editing ../../src/Auto485.* and re-running +; upload immediately tests the new code. +; +; pio run -d extras/hardware_test -e uno -t upload +; pio device monitor -b 9600 +; +; (Run from the repo root. Or `cd extras/hardware_test` and drop the `-d`.) +; +; Note: meaningfully exercising the bus needs a second RS485 node (or a USB +; RS485 adapter) to talk to; without one this is a compile/upload/boot smoke +; test. + +[env:uno] +platform = atmelavr +board = uno +framework = arduino +lib_extra_dirs = ${PROJECT_DIR}/../../.. +lib_deps = Auto485 +monitor_speed = 9600 diff --git a/extras/hardware_test/src/main.cpp b/extras/hardware_test/src/main.cpp new file mode 100644 index 0000000..4e6a406 --- /dev/null +++ b/extras/hardware_test/src/main.cpp @@ -0,0 +1,53 @@ +/* + Hardware smoke-test for Auto485. + + Drives a half-duplex RS485 transceiver (e.g. a MAX485) over the hardware + Serial port, exercising both directions of the automatic DE/RE switching: + + - every second it transmits a heartbeat line (write + println), which + flips the driver to TX and back to RX automatically + - any bytes received are echoed straight back, and the trailing println() + returns the bus to RX + + Wiring: Arduino pin 2 -> MAX485 DE and RE (tied together), Serial TX/RX -> + the MAX485 DI/RO. Connect A/B to a second RS485 node or a USB RS485 adapter + to see the traffic; without a peer this simply confirms the sketch builds, + uploads and boots against the local source. + + Upload + monitor: + pio run -d extras/hardware_test -e uno -t upload + pio device monitor -b 9600 +*/ + +#include +#include + +const int DE_PIN = 2; + +Auto485 bus(DE_PIN); // pin 2 -> MAX485 DE and RE + +void setup() +{ + bus.begin(9600); // must match the speed of the other node +} + +unsigned long last_beat = 0; + +void loop() +{ + // Echo anything received back onto the bus. + if (bus.available() > 0) + { + while (bus.available() > 0) + bus.write(bus.read()); // switches to TX + bus.println(); // completes the line and returns to RX + } + + // Transmit a heartbeat once a second. + if (millis() - last_beat >= 1000) + { + last_beat = millis(); + bus.print("beat "); + bus.println(last_beat / 1000); // returns to RX when done + } +} diff --git a/library.json b/library.json index 650c3a0..f587a1b 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Auto485", - "version": "1.0.0", + "version": "1.1.0", "description": "Auto485 - a small helper library that takes some of the tedium out of RS485 communication. It automatically handles the DE/RE pin toggling for half-duplex RS485 transceivers.", "keywords": "RS485, communication, serial, auto485, arduino", "repository": { @@ -17,9 +17,5 @@ "license": "MIT", "frameworks": "arduino", "platforms": "*", - "headers": "Auto485.h", - "build": { - "srcDir": ".", - "includeDir": "." - } + "headers": "Auto485.h" } diff --git a/library.properties b/library.properties index 2fe07b1..75657b7 100644 --- a/library.properties +++ b/library.properties @@ -1,7 +1,7 @@ name=Auto485 -version=1.0.0 -author=Michael Adams -maintainer=Michael Adams +version=1.1.0 +author=Michael Adams +maintainer=Michael Adams sentence=Helper library for RS485 communication paragraph=Auto485 is a small helper library that takes some of the tedium out of RS485 communication. It automatically handles the DE/RE pin toggling for half-duplex RS485 transceivers. category=Communication diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..81169ab --- /dev/null +++ b/platformio.ini @@ -0,0 +1,18 @@ +; PlatformIO project configuration for Auto485. +; +; pio test -e native -> run the native unit tests (mocks the Arduino Serial) +; pio run -e uno -> compile-check against AVR (Arduino Uno) + +[platformio] +src_dir = src + +[env:native] +platform = native +lib_compat_mode = off +test_build_src = true +build_flags = -I test/mock + +[env:uno] +platform = atmelavr +board = uno +framework = arduino diff --git a/Auto485.cpp b/src/Auto485.cpp similarity index 100% rename from Auto485.cpp rename to src/Auto485.cpp diff --git a/Auto485.h b/src/Auto485.h similarity index 100% rename from Auto485.h rename to src/Auto485.h diff --git a/test/mock/Arduino.h b/test/mock/Arduino.h new file mode 100644 index 0000000..44770fd --- /dev/null +++ b/test/mock/Arduino.h @@ -0,0 +1,246 @@ +/* + Minimal Arduino.h mock for native unit tests. + + Provides just enough of the Arduino runtime for Auto485 to build and run + off-device. Auto485 is a Stream wrapper that flips a transceiver's DE/RE + pins between transmit and receive, so the mock supplies: + + - pinMode/digitalWrite that record pin state the test can inspect + - the Print/Stream base classes Auto485 derives from + - a concrete HardwareSerial backed by an output buffer and an input queue +*/ + +#ifndef _Auto485_test_Arduino_h +#define _Auto485_test_Arduino_h + +#include +#include +#include +#include +#include +#include + +// Pin levels and modes. +#define LOW 0 +#define HIGH 1 +#define INPUT 0 +#define OUTPUT 1 +#define INPUT_PULLUP 2 + +// Number bases for print(). +#define DEC 10 +#define HEX 16 +#define OCT 8 +#define BIN 2 + +// Serial config token; the mock never interprets it. +#define SERIAL_8N2 0x0E + +// Recorded pin state, inspected by the tests. Defined in the test translation +// unit. +extern std::map pin_modes; +extern std::map pin_states; + +inline void pinMode(int pin, int mode) +{ + pin_modes[pin] = mode; +} + +inline void digitalWrite(int pin, int value) +{ + pin_states[pin] = value ? HIGH : LOW; +} + +inline unsigned long millis() +{ + return 0; +} + +// Forward declarations for the print() overloads Auto485 references. +class __FlashStringHelper; +class Print; + +class String +{ + std::string _s; + + public: + String() + { + } + String(const char *s) + : _s(s) + { + } + const char *c_str() const + { + return _s.c_str(); + } +}; + +class Printable +{ + public: + virtual size_t printTo(Print &) const = 0; +}; + +// Minimal Print: everything is funnelled through the pure-virtual write(), so +// deriving classes (Auto485) see a write() call for every byte printed - which +// is exactly what drives its TX/RX mode switching. +class Print +{ + public: + virtual size_t write(uint8_t) = 0; + virtual void flush() + { + } + + size_t print(const char *s) + { + size_t n = 0; + while (*s) + n += write((uint8_t)*s++); + return n; + } + size_t print(char c) + { + return write((uint8_t)c); + } + size_t print(const String &s) + { + return print(s.c_str()); + } + size_t print(const __FlashStringHelper *) + { + return 0; + } + size_t print(unsigned char b, int = DEC) + { + return write(b); + } + size_t print(int n, int = DEC) + { + return print_number(n); + } + size_t print(unsigned int n, int = DEC) + { + return print_number((long)n); + } + size_t print(long n, int = DEC) + { + return print_number(n); + } + size_t print(unsigned long n, int = DEC) + { + return print_number((long)n); + } + size_t print(double, int = 2) + { + return write('0'); + } + size_t print(const Printable &x) + { + return x.printTo(*this); + } + size_t println() + { + return write('\r') + write('\n'); + } + + private: + size_t print_number(long n) + { + char buf[16]; + size_t len = 0; + if (n < 0) + { + write('-'); + len++; + n = -n; + } + int i = 0; + do + { + buf[i++] = '0' + (n % 10); + n /= 10; + } while (n); + while (i > 0) + len += write((uint8_t)buf[--i]); + return len; + } +}; + +class Stream : public Print +{ + public: + virtual int available() = 0; + virtual int read() = 0; + virtual int peek() = 0; +}; + +// A concrete stand-in for the Arduino HardwareSerial. tx collects everything +// written; rx holds bytes the test feeds in for read(). +class HardwareSerial : public Stream +{ + public: + std::vector tx; + std::deque rx; + int flush_count = 0; + bool started = false; + bool ended = false; + unsigned long baud = 0; + + void begin(unsigned long b) + { + started = true; + ended = false; + baud = b; + } + void begin(unsigned long b, uint8_t) + { + begin(b); + } + void end() + { + ended = true; + started = false; + } + + size_t write(uint8_t c) + { + tx.push_back(c); + return 1; + } + void flush() + { + flush_count++; + } + int available() + { + return (int)rx.size(); + } + int read() + { + if (rx.empty()) + return -1; + uint8_t c = rx.front(); + rx.pop_front(); + return c; + } + int peek() + { + return rx.empty() ? -1 : rx.front(); + } + + // Test helper: enqueue a byte as if it had arrived over the wire. + void feed(uint8_t c) + { + rx.push_back(c); + } +}; + +// The default argument of the Auto485 constructor references Serial; the test +// translation unit defines it. +extern HardwareSerial Serial; + +#endif diff --git a/test/test_auto485/test_main.cpp b/test/test_auto485/test_main.cpp new file mode 100644 index 0000000..ff3d8a1 --- /dev/null +++ b/test/test_auto485/test_main.cpp @@ -0,0 +1,186 @@ +/* + Native unit tests for Auto485. + + These run off-device via PlatformIO's `native` platform. The Arduino + runtime is mocked (see test/mock/Arduino.h): pinMode/digitalWrite record pin + state and HardwareSerial is a concrete buffer. Auto485's whole job is to + drive a half-duplex RS485 transceiver's DE/RE pins - HIGH to transmit, LOW + to receive - so the tests write/flush through the wrapper and assert on the + recorded pin levels and the bytes handed to the underlying serial port. + + Pin convention: DE (and RE, if separate) is HIGH in TX mode, LOW in RX. +*/ + +#include + +#include "Arduino.h" +#include "Auto485.h" + +// Referenced by the Auto485 constructor's default argument. The tests always +// pass an explicit serial port, but the symbol still has to exist. +HardwareSerial Serial; + +// Storage for the pin state the mock records. +std::map pin_modes; +std::map pin_states; + +const int DE_PIN = 2; +const int RE_PIN = 3; + +void setUp(void) +{ + pin_modes.clear(); + pin_states.clear(); +} + +void tearDown(void) +{ +} + +// --- setup / teardown ------------------------------------------------------ + +// begin() configures the DE pin as an output and starts the serial port. +void test_begin_configures_pin_and_serial(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, -1, serial); + + bus.begin(9600); + + TEST_ASSERT_EQUAL_INT(OUTPUT, pin_modes[DE_PIN]); + TEST_ASSERT_TRUE(serial.started); + TEST_ASSERT_EQUAL_UINT32(9600, serial.baud); +} + +// end() returns the transceiver to receive mode and stops the serial port. +void test_end_returns_to_receive(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, -1, serial); + + bus.begin(9600); + bus.write('A'); // now transmitting (DE HIGH) + bus.end(); + + TEST_ASSERT_EQUAL_INT(LOW, pin_states[DE_PIN]); + TEST_ASSERT_TRUE(serial.ended); +} + +// --- transmit / receive switching ------------------------------------------ + +// write() flips into transmit mode and forwards the byte to the serial port. +void test_write_enables_transmit(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, -1, serial); + bus.begin(9600); + + bus.write('A'); + + TEST_ASSERT_EQUAL_INT(HIGH, pin_states[DE_PIN]); + TEST_ASSERT_EQUAL_UINT(1u, serial.tx.size()); + TEST_ASSERT_EQUAL_UINT8('A', serial.tx[0]); +} + +// flush() drains the serial port and drops back to receive mode. +void test_flush_returns_to_receive(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, -1, serial); + bus.begin(9600); + + bus.write('A'); + bus.flush(); + + TEST_ASSERT_EQUAL_INT(LOW, pin_states[DE_PIN]); + TEST_ASSERT_TRUE(serial.flush_count > 0); +} + +// A trailing println() sends its bytes then returns the bus to receive mode, +// so the line is complete before anyone else can drive the bus. +void test_println_returns_to_receive(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, -1, serial); + bus.begin(9600); + + bus.println("hi"); + + TEST_ASSERT_EQUAL_INT(LOW, pin_states[DE_PIN]); + // 'h', 'i', '\r', '\n' + TEST_ASSERT_EQUAL_UINT(4u, serial.tx.size()); + TEST_ASSERT_EQUAL_UINT8('h', serial.tx[0]); + TEST_ASSERT_EQUAL_UINT8('i', serial.tx[1]); +} + +// Manual set_mode(): a TX -> RX transition flushes the serial port so the last +// bytes clear the shift register before the driver is disabled. +void test_set_mode_tx_to_rx_flushes(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, -1, serial); + bus.begin(9600); + + bus.set_mode(Auto485::TX); + TEST_ASSERT_EQUAL_INT(HIGH, pin_states[DE_PIN]); + int before = serial.flush_count; + + bus.set_mode(Auto485::RX); + TEST_ASSERT_EQUAL_INT(LOW, pin_states[DE_PIN]); + TEST_ASSERT_TRUE(serial.flush_count > before); +} + +// --- separate DE / RE pins ------------------------------------------------- + +// With distinct DE and RE pins, both are configured as outputs and toggle +// together with the mode. +void test_separate_de_re_pins_toggle_together(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, RE_PIN, serial); + bus.begin(9600); + + TEST_ASSERT_EQUAL_INT(OUTPUT, pin_modes[DE_PIN]); + TEST_ASSERT_EQUAL_INT(OUTPUT, pin_modes[RE_PIN]); + + bus.write('A'); + TEST_ASSERT_EQUAL_INT(HIGH, pin_states[DE_PIN]); + TEST_ASSERT_EQUAL_INT(HIGH, pin_states[RE_PIN]); + + bus.flush(); + TEST_ASSERT_EQUAL_INT(LOW, pin_states[DE_PIN]); + TEST_ASSERT_EQUAL_INT(LOW, pin_states[RE_PIN]); +} + +// --- receive path ---------------------------------------------------------- + +// Reads pass straight through to the underlying serial port. +void test_read_passthrough(void) +{ + HardwareSerial serial; + Auto485 bus(DE_PIN, -1, serial); + bus.begin(9600); + + serial.feed('X'); + serial.feed('Y'); + + TEST_ASSERT_EQUAL_INT(2, bus.available()); + TEST_ASSERT_EQUAL_INT('X', bus.peek()); + TEST_ASSERT_EQUAL_INT('X', bus.read()); + TEST_ASSERT_EQUAL_INT('Y', bus.read()); + TEST_ASSERT_EQUAL_INT(-1, bus.read()); +} + +int main(int, char **) +{ + UNITY_BEGIN(); + RUN_TEST(test_begin_configures_pin_and_serial); + RUN_TEST(test_end_returns_to_receive); + RUN_TEST(test_write_enables_transmit); + RUN_TEST(test_flush_returns_to_receive); + RUN_TEST(test_println_returns_to_receive); + RUN_TEST(test_set_mode_tx_to_rx_flushes); + RUN_TEST(test_separate_de_re_pins_toggle_together); + RUN_TEST(test_read_passthrough); + return UNITY_END(); +}