diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..427feac --- /dev/null +++ b/.clang-format @@ -0,0 +1,13 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +UseTab: ForIndentation +TabWidth: 4 +IndentWidth: 4 +BreakBeforeBraces: Allman +ColumnLimit: 0 +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +SortIncludes: false +... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..146aa42 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +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: + fqbn: + - arduino:avr:uno + - arduino:avr:mega + steps: + - uses: actions/checkout@v7 + - name: Compile example sketches + uses: arduino/compile-sketches@v1 + with: + fqbn: ${{ matrix.fqbn }} + platforms: | + - name: arduino:avr + libraries: | + - source-path: ./ + - source-url: https://github.com/madleech/Auto485.git + sketch-paths: | + - examples/hello_world + - examples/inputs_and_outputs + - examples/many_inputs + - examples/many_outputs + - examples/multiple_nodes + - examples/multiple_nodes_inputting + - examples/rs485_cmri_hello_world + - examples/rs485_rx_and_tx + - examples/susic_256_outputs + + 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 index c583f98..2c11a97 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,15 @@ pip-log.txt # Mac crap .DS_Store + + +############# +## PlatformIO +############# + +.pio/ +.pioenvs/ +.piolibdeps/ +.vscode/ +*.o +*.a diff --git a/CMRI.cpp b/CMRI.cpp deleted file mode 100644 index e8c321d..0000000 --- a/CMRI.cpp +++ /dev/null @@ -1,254 +0,0 @@ -/* - CMRI - a small library for Arduino to interface with the C/MRI - computer control system for model railroads - Copyright (C) 2012 Michael Adams (www.michael.net.nz) - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -*/ - -#include "CMRI.h" -#include - -CMRI::CMRI(unsigned int address, unsigned int input_bits, unsigned int output_bits, Stream &serial_class) - // store details -: _address(address) -, _rx_length((output_bits + 7) / 8) -, _tx_length((input_bits + 7) / 8) - - // set state -, _rx_buffer((char *) malloc(_rx_length)) -, _tx_buffer((char *) malloc(_tx_length)) - -, _serial(serial_class) - - // parsing state -, _mode(PREAMBLE_1) -, _rx_index(0) - -{ - // clear to zero - for(int i=0; i<_rx_length; i++) - _rx_buffer[i] = 0; - for(int i=0; i<_tx_length; i++) - _tx_buffer[i] = 0; -} - -void CMRI::set_address(unsigned int address) -{ - _address = address; -} - -// reads in serial data, decodes packets -// automatically responds to POLL requests -// returns packet type so if we got a SET request you know to update your outputs -bool CMRI::process() -{ - while (_serial.available() > 0) - { - if (process_char(_serial.read())) - { - return true; - } - } - return false; -} - -bool CMRI::process_char(char c) -{ - // if it's a SET that's fine do nothing - // if it's an INIT that's also fine, we don't really care - // if it's a GET, well, do nothing since it must be someone else replying - // if it's a POLL then reply straight away with our data - switch (_decode(c)) - { - case POLL: - transmit(); - return true; - - case SET: - return true; - - default: - return false; - } -} - - - -// public methods - -bool CMRI::get_bit(int pos) -{ - // 1: divide index by 8 to get byte offset - char c = get_byte(pos / 8); - // 2: return bit at that location - return (bool) ((c >> (pos % 8)) & 0x01); -} - -char CMRI::get_byte(int pos) -{ - if (pos >= _rx_length) - return 0; // out of bounds - else - return _rx_buffer[pos]; -} - -bool CMRI::set_bit(int pos, bool bit) -{ - if ((pos + 7) / 8 >= _tx_length) - return false; // out of bounds - else - { - int index = pos / 8; - _tx_buffer[index] = bit - ? _tx_buffer[index] | 1 << pos % 8 // if bit=1, then OR it - : _tx_buffer[index] & ~(1 << pos % 8) // if bit=0, then NAND it - ; - return true; - } -} - -bool CMRI::set_byte(int pos, char b) -{ - if (pos >= _tx_length) - return false; // out of bounds - else - { - _tx_buffer[pos] = b; - return true; - } -} - - -void CMRI::transmit() -{ - delayMicroseconds(50); //a minscule delay to let things recover - _serial.write(255); - _serial.write(255); - _serial.write(STX); - _serial.write(65 + _address); - _serial.write(GET); - for (int i=0; i<_tx_length; i++) - { - if (_tx_buffer[i] == ETX) - _serial.write(ESC); // escape because this looks like an STX bit (very basic protocol) - if (_tx_buffer[i] == ESC) - _serial.write(ESC); // escape because this looks like an escape bit (very basic protocol) - _serial.write(_tx_buffer[i]); - } - _serial.write(ETX); - _serial.flush(); -} - -// Private methods -uint8_t CMRI::_decode(uint8_t c) -{ - switch(_mode) - { - case PREAMBLE_1: - _rx_index = 0; - if (c == 0xFF) - _mode = PREAMBLE_2; - break; - - case PREAMBLE_2: - if (c == 0xFF) - _mode = PREAMBLE_3; - else - _mode = PREAMBLE_1; - break; - - case PREAMBLE_3: - if (c == STX) - _mode = DECODE_ADDR; - else - _mode = PREAMBLE_1; - break; - - case DECODE_ADDR: - if (c == 'A' + _address) - _mode = DECODE_CMD; - else if (c >= 'A') - _mode = IGNORE_CMD; - else - _mode = PREAMBLE_1; - break; - - case DECODE_CMD: - if (c == SET) - _mode = DECODE_DATA; - else if (c == POLL) - goto POSTAMBLE_POLL; - else - _mode = POSTAMBLE_OTHER; - break; - - case IGNORE_CMD: - _mode = IGNORE_DATA; - break; - - case DECODE_DATA: - if (c == ESC) - _mode = DECODE_ESC_DATA; - else if (c == ETX) - goto POSTAMBLE_SET; - else if (_rx_index >= _rx_length) - { } - else - _rx_buffer[_rx_index++] = c; - break; - - case DECODE_ESC_DATA: - if (_rx_index >= _rx_length) - { } - else - _rx_buffer[_rx_index++] = c; - _mode = DECODE_DATA; - break; - - case IGNORE_DATA: - if (c == ESC) - _mode = IGNORE_ESC_DATA; - else if (c == ETX) - goto POSTAMBLE_IGNORE; - break; - - case IGNORE_ESC_DATA: - _mode = IGNORE_DATA; - break; - - case POSTAMBLE_OTHER: - _mode = PREAMBLE_1; - break; - } - return NOOP; - -POSTAMBLE_SET: - _mode = PREAMBLE_1; - return SET; - -POSTAMBLE_POLL: - _mode = PREAMBLE_1; - return POLL; - -POSTAMBLE_IGNORE: - _mode = PREAMBLE_1; - return NOOP; -} diff --git a/CMRI.h b/CMRI.h deleted file mode 100644 index 254e723..0000000 --- a/CMRI.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - CMRI - a small library for Arduino to interface with the C/MRI - computer control system for model railroads - Copyright (C) 2012 Michael Adams (www.michael.net.nz) - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -*/ - -#ifndef CMRI_h -#define CMRI_h - -#define _CMRI_VERSION 1.5.1 // version of this library -#include - -class CMRI -{ - public: - CMRI(unsigned int address = 0, unsigned int input_bits = 24, unsigned int output_bits = 48, Stream& serial_class = Serial); - void set_address(unsigned int address); - - bool process(); - bool process_char(char c); - void transmit(); - - bool get_bit(int n); - char get_byte(int n); - - bool set_bit(int n, bool b); - bool set_byte(int n, char b); - - enum { - MAX = 258, // max packet length in bytes (64 i/o cards @ 32 bits each + packet type and address bytes) - INIT = 'I', // PC is telling us stuff we don't really care about - SET = 'T', // as in TX from the PC => Arduino, PC is SETing our status - GET = 'R', // as in TX from Arduino => PC, PC is GETing our status - POLL = 'P', // PC wants to know our status - NOOP = 0x00, // do nothing - STX = 0x02, // start byte - ETX = 0x03, // end byte - ESC = 0x10, // escape byte - }; - -private: - enum {PREAMBLE_1,PREAMBLE_2,PREAMBLE_3,DECODE_ADDR,DECODE_CMD,DECODE_DATA,DECODE_ESC_DATA,IGNORE_CMD,IGNORE_DATA,IGNORE_ESC_DATA,POSTAMBLE_SET,POSTAMBLE_POLL,POSTAMBLE_OTHER}; - - int _address; - int _rx_length; - int _tx_length; - char _rx_packet_type; - char* _rx_buffer; - char* _tx_buffer; - - Stream& _serial; - - // parsing state variables - int _mode; - int _rx_index; - - uint8_t _decode(uint8_t c); // process one character received from serial port -}; - -#endif diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..81df601 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Michael Adams + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/hello_world/hello_world.ino b/examples/hello_world/hello_world.ino index 1bf0aec..c72f9ac 100644 --- a/examples/hello_world/hello_world.ino +++ b/examples/hello_world/hello_world.ino @@ -3,12 +3,12 @@ * ================================= * Sets up pin 13 (LED) as an output, and attaches it to the first output bit * of the emulated SMINI interface. - * + * * To set up in JMRI: - * 1: Create a new connection, - * - type = C/MRI, - * - connection = Serial, - * - port = , + * 1: Create a new connection, + * - type = C/MRI, + * - connection = Serial, + * - port = , * - speed = 9600 * 2: Click 'Configure C/MRI nodes' and create a new SMINI node * 3: Click 'Add Node' and then 'Done' @@ -16,12 +16,12 @@ * 5: Open Tools > Tables > Lights and click 'Add' * 6: Add a new light at hardware address 1, then click 'Create' and close the window. Ignore the save message. * 7: Click the 'Off' state button to turn the LED on. Congratulations! - * + * * Debugging: * Open the CMRI > CMRI Monitor window to check what is getting sent. * With 'Show raw data' turned on the output looks like: - * [41 54 01 00 00 00 00 00] Transmit ua=0 OB=1 0 0 0 0 0 - * + * [41 54 01 00 00 00 00 00] Transmit ua=0 OB=1 0 0 0 0 0 + * * 0x41 = 65 = A = address 0 * 0x54 = 84 = T = transmit, i.e. PC -> C/MRI * 0x01 = 0b00000001 = turn on the 1st bit @@ -32,16 +32,17 @@ CMRI cmri; // defaults to a SMINI with address 0. SMINI = 24 inputs, 48 outputs -void setup() { - Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI - pinMode(13, OUTPUT); +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI + pinMode(13, OUTPUT); } -void loop() { - // 1: main processing node of cmri library - cmri.process(); - - // 2: update output. Reads bit 0 of T packet and sets the LED to this - digitalWrite(13, cmri.get_bit(0)); -} +void loop() +{ + // 1: main processing node of cmri library + cmri.process(); + // 2: update output. Reads bit 0 of T packet and sets the LED to this + digitalWrite(13, cmri.get_bit(0)); +} diff --git a/examples/inputs_and_outputs/inputs_and_outputs.ino b/examples/inputs_and_outputs/inputs_and_outputs.ino index d9b7d89..12ad311 100644 --- a/examples/inputs_and_outputs/inputs_and_outputs.ino +++ b/examples/inputs_and_outputs/inputs_and_outputs.ino @@ -2,7 +2,7 @@ * An example of C/MRI inputs and outputs * ====================================== * Sets up pins 2-5 as an outputs with LEDs, and pins 6-9 as inputs with pullups. - * + * * 1: Set up a JMRI connection, see hello_world, steps 1-4 * 2: Open Tools > Tables > Lights and click 'Add' * 3: Add a new light at hardware address 1, then click 'Create'. @@ -11,18 +11,18 @@ * 6: You'll notice the TX and RX LEDs burst into life. This is JMRI polling the state of our sensors. * 7: Ground pin 6, you'll see sensor #1 go Active, while the rest are Inactive. * 8: Switch to Lights and play around with the State buttons. Congratulations! - * + * * Debugging: * Open the CMRI > CMRI Monitor window to check what is getting sent and received. * With 'Show raw data' turned on the output looks like: * [41 50] Poll ua=0 - * [41 52 01 00 00] Receive ua=0 IB=1 0 0 - * + * [41 52 01 00 00] Receive ua=0 IB=1 0 0 + * * 0x41 = 65 = A = address 0 * 0x50 = 80 = P = poll, i.e. PC asking C/MRI to transmit its state back to PC - * + * * 0x41 = 65 = A = address 0 - * 0x52 = 82 = R = receive, i.e. PC receiving state data from C/MRI + * 0x52 = 82 = R = receive, i.e. PC receiving state data from C/MRI * 0x01 = 0b00000001 = 1st bit is high * 0x00 = 0b00000000 = all other bits off */ @@ -31,28 +31,34 @@ CMRI cmri; // defaults to a SMINI with address 0. SMINI = 24 inputs, 48 outputs -void setup() { - Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI - for (int i=2; i<=5; i++) { pinMode(i, OUTPUT); } - for (int i=6; i<=9; i++) { pinMode(i, INPUT); digitalWrite(i, HIGH); } -} - -void loop() { - // 1: build up a packet - cmri.process(); - - // 2: update outputs - digitalWrite(2, cmri.get_bit(0)); - digitalWrite(3, cmri.get_bit(1)); - digitalWrite(4, cmri.get_bit(2)); - digitalWrite(5, cmri.get_bit(3)); - - // 3: update inputs (invert digitalRead due to the pullups) - cmri.set_bit(0, !digitalRead(6)); - cmri.set_bit(1, !digitalRead(7)); - cmri.set_bit(2, !digitalRead(8)); - cmri.set_bit(3, !digitalRead(9)); +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI + for (int i = 2; i <= 5; i++) + { + pinMode(i, OUTPUT); + } + for (int i = 6; i <= 9; i++) + { + pinMode(i, INPUT); + digitalWrite(i, HIGH); + } } +void loop() +{ + // 1: build up a packet + cmri.process(); + // 2: update outputs + digitalWrite(2, cmri.get_bit(0)); + digitalWrite(3, cmri.get_bit(1)); + digitalWrite(4, cmri.get_bit(2)); + digitalWrite(5, cmri.get_bit(3)); + // 3: update inputs (invert digitalRead due to the pullups) + cmri.set_bit(0, !digitalRead(6)); + cmri.set_bit(1, !digitalRead(7)); + cmri.set_bit(2, !digitalRead(8)); + cmri.set_bit(3, !digitalRead(9)); +} diff --git a/examples/many_inputs/many_inputs.ino b/examples/many_inputs/many_inputs.ino index 3dd750d..4a938a6 100644 --- a/examples/many_inputs/many_inputs.ino +++ b/examples/many_inputs/many_inputs.ino @@ -2,29 +2,29 @@ #include // pins for a 168/368 based Arduino -#define SS 10 -#define MOSI 11 /* not used */ -#define MISO 12 +#define SS 10 +#define MOSI 11 /* not used */ +#define MISO 12 #define CLOCK 13 CMRI cmri; // defaults to a SMINI with address 0. SMINI = 24 inputs, 48 outputs -void setup() { - Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI - SPI.begin(); +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI + SPI.begin(); } -void loop() { - // 1: main processing node of cmri library - cmri.process(); - - // 2: toggle the SS pin - digitalWrite(SS, HIGH); - delay(1); // wait while data CD4021 loads in data - digitalWrite(SS, LOW); - - // 3: update input status in CMRI, will get sent to PC next time we're asked - cmri.set_byte(0, SPI.transfer(0x00 /* dummy output value */)); -} +void loop() +{ + // 1: main processing node of cmri library + cmri.process(); + // 2: toggle the SS pin + digitalWrite(SS, HIGH); + delay(1); // wait while data CD4021 loads in data + digitalWrite(SS, LOW); + // 3: update input status in CMRI, will get sent to PC next time we're asked + cmri.set_byte(0, SPI.transfer(0x00 /* dummy output value */)); +} diff --git a/examples/many_outputs/many_outputs.ino b/examples/many_outputs/many_outputs.ino index 660519d..67520c7 100644 --- a/examples/many_outputs/many_outputs.ino +++ b/examples/many_outputs/many_outputs.ino @@ -2,25 +2,25 @@ #define LATCH 8 #define CLOCK 12 -#define DATA 11 +#define DATA 11 CMRI cmri; // defaults to a SMINI with address 0. SMINI = 24 inputs, 48 outputs -void setup() { - Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI - pinMode(LATCH, OUTPUT); - pinMode(CLOCK, OUTPUT); - pinMode(DATA, OUTPUT); -} - -void loop() { - // 1: main processing node of cmri library - cmri.process(); - - // 2: update output. Reads bit 0 of T packet and sets the LED to this - digitalWrite(LATCH, LOW); - shiftOut(DATA, CLOCK, MSBFIRST, cmri.get_byte(0)); - digitalWrite(LATCH, HIGH); +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI + pinMode(LATCH, OUTPUT); + pinMode(CLOCK, OUTPUT); + pinMode(DATA, OUTPUT); } +void loop() +{ + // 1: main processing node of cmri library + cmri.process(); + // 2: update output. Reads bit 0 of T packet and sets the LED to this + digitalWrite(LATCH, LOW); + shiftOut(DATA, CLOCK, MSBFIRST, cmri.get_byte(0)); + digitalWrite(LATCH, HIGH); +} diff --git a/examples/multiple_nodes/multiple_nodes.ino b/examples/multiple_nodes/multiple_nodes.ino index 5151212..f3bcf0a 100644 --- a/examples/multiple_nodes/multiple_nodes.ino +++ b/examples/multiple_nodes/multiple_nodes.ino @@ -2,12 +2,12 @@ * An example of driving multiple C/MRI nodes on a single Arduino * ============================================================== * Sets up pins 2 and 3 as outputs, and attaches each to a different SMINI node. - * + * * To set up in JMRI: - * 1: Create a new connection, - * - type = C/MRI, - * - connection = Serial, - * - port = , + * 1: Create a new connection, + * - type = C/MRI, + * - connection = Serial, + * - port = , * - speed = 9600 * 2: Click 'Configure C/MRI nodes' and click 'Add Node' to create a new SMINI node * 3: In the same window, type in '1' as the address and click 'Add Node' to add a second SMINI node @@ -24,23 +24,25 @@ CMRI cmri0(0); // first SMINI, 24 inputs, 48 outputs CMRI cmri1(1); // second SMINI, another 24 inputs and another 48 outputs -void setup() { - Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI - pinMode(2, OUTPUT); - pinMode(3, OUTPUT); +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI + pinMode(2, OUTPUT); + pinMode(3, OUTPUT); } char c; -void loop() { - // 1: main processing node of cmri library - while (Serial.available() > 0) - { - c = Serial.read(); - cmri0.process_char(c); - cmri1.process_char(c); - } - - // 2: update outputs. - digitalWrite(2, cmri0.get_bit(0)); - digitalWrite(3, cmri1.get_bit(0)); +void loop() +{ + // 1: main processing node of cmri library + while (Serial.available() > 0) + { + c = Serial.read(); + cmri0.process_char(c); + cmri1.process_char(c); + } + + // 2: update outputs. + digitalWrite(2, cmri0.get_bit(0)); + digitalWrite(3, cmri1.get_bit(0)); } diff --git a/examples/multiple_nodes_inputting/multiple_nodes_inputting.ino b/examples/multiple_nodes_inputting/multiple_nodes_inputting.ino index 25643a6..3cb3d6e 100644 --- a/examples/multiple_nodes_inputting/multiple_nodes_inputting.ino +++ b/examples/multiple_nodes_inputting/multiple_nodes_inputting.ino @@ -2,12 +2,12 @@ * An example of driving multiple C/MRI nodes on a single Arduino * ============================================================== * Sets up pins 2 and 3 as outputs, and attaches each to a different SMINI node. - * + * * To set up in JMRI: - * 1: Create a new connection, - * - type = C/MRI, - * - connection = Serial, - * - port = , + * 1: Create a new connection, + * - type = C/MRI, + * - connection = Serial, + * - port = , * - speed = 9600 * 2: Click 'Configure C/MRI nodes' and click 'Add Node' to create a new SMINI node * 3: In the same window, type in '1' as the address and click 'Add Node' to add a second SMINI node @@ -24,25 +24,27 @@ CMRI cmri0(0); // first SMINI, 24 inputs, 48 outputs CMRI cmri1(1); // second SMINI, another 24 inputs and another 48 outputs -void setup() { - Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI - - cmri0.set_bit(0, HIGH); // system name CS0001 - cmri0.set_bit(22, LOW); // system name CS0023 - cmri0.set_bit(23, HIGH); // system name CS0024 - - cmri1.set_bit(0, HIGH); // system name CS1001 - cmri1.set_bit(1, HIGH); // system name CS1002 - cmri1.set_bit(22, HIGH); // system name CS10023 +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI + + cmri0.set_bit(0, HIGH); // system name CS0001 + cmri0.set_bit(22, LOW); // system name CS0023 + cmri0.set_bit(23, HIGH); // system name CS0024 + + cmri1.set_bit(0, HIGH); // system name CS1001 + cmri1.set_bit(1, HIGH); // system name CS1002 + cmri1.set_bit(22, HIGH); // system name CS10023 } char c; -void loop() { - // 1: main processing node of cmri library - while (Serial.available() > 0) - { - c = Serial.read(); - cmri0.process_char(c); - cmri1.process_char(c); - } +void loop() +{ + // 1: main processing node of cmri library + while (Serial.available() > 0) + { + c = Serial.read(); + cmri0.process_char(c); + cmri1.process_char(c); + } } diff --git a/examples/rs485_cmri_hello_world/rs485_cmri_hello_world.ino b/examples/rs485_cmri_hello_world/rs485_cmri_hello_world.ino index e6b8f88..2f074ca 100644 --- a/examples/rs485_cmri_hello_world/rs485_cmri_hello_world.ino +++ b/examples/rs485_cmri_hello_world/rs485_cmri_hello_world.ino @@ -4,34 +4,36 @@ * Uses an RS485 bus to transparently talk support multiple ArduinoCMRI nodes on one bus. * By passing in an Auto485 object to the CMRI constructor, we are able to automatically * control the DE and RE pins on our RS485 bus transceiver. - * - * Sets up pin 13 (LED) as an output, pin 12 as an input, and attaches both to + * + * Sets up pin 13 (LED) as an output, pin 12 as an input, and attaches both to * the first output/input bits of the emulated SMINI interface. - * + * * To set up in JMRI, follow instructions in hello_world. - * + * */ #include #include -Auto485 bus(2); // Arduino pin 2 -> MAX485 DE and RE pins +Auto485 bus(2); // Arduino pin 2 -> MAX485 DE and RE pins CMRI cmri(0, 24, 48, bus); // sets up an SMINI with address 0. SMINI = 24 inputs, 48 outputs -void setup() { - bus.begin(9600, SERIAL_8N2); // open the RS485 bus at 9600bps - pinMode(12, INPUT); digitalWrite(12, HIGH); - pinMode(13, OUTPUT); +void setup() +{ + bus.begin(9600, SERIAL_8N2); // open the RS485 bus at 9600bps + pinMode(12, INPUT); + digitalWrite(12, HIGH); + pinMode(13, OUTPUT); } -void loop() { - // 1: main processing node of cmri library - cmri.process(); - - // 2: update output. Reads bit 0 of T packet and sets the LED to this - digitalWrite(13, cmri.get_bit(0)); - - // 3: update inputs - cmri.set_bit(0, !digitalRead(12)); -} +void loop() +{ + // 1: main processing node of cmri library + cmri.process(); + + // 2: update output. Reads bit 0 of T packet and sets the LED to this + digitalWrite(13, cmri.get_bit(0)); + // 3: update inputs + cmri.set_bit(0, !digitalRead(12)); +} diff --git a/examples/rs485_rx_and_tx/rs485_rx_and_tx.ino b/examples/rs485_rx_and_tx/rs485_rx_and_tx.ino index 3ee9dea..c85787b 100644 --- a/examples/rs485_rx_and_tx/rs485_rx_and_tx.ino +++ b/examples/rs485_rx_and_tx/rs485_rx_and_tx.ino @@ -3,25 +3,26 @@ #define CMRI_ADDR 0 -#define DE_PIN 2 -#define LED_PIN 3 +#define DE_PIN 2 +#define LED_PIN 3 -Auto485 bus(DE_PIN); // Arduino pin 2 -> MAX485 DE and RE pins +Auto485 bus(DE_PIN); // Arduino pin 2 -> MAX485 DE and RE pins CMRI cmri(CMRI_ADDR, 24, 48, bus); // defaults to a SMINI with address 0. SMINI = 24 inputs, 48 outputs -void setup() { - bus.begin(9600, SERIAL_8N2); // open RS485 bus at 9600bps - pinMode(LED_PIN, OUTPUT); +void setup() +{ + bus.begin(9600, SERIAL_8N2); // open RS485 bus at 9600bps + pinMode(LED_PIN, OUTPUT); } -void loop() { - // 1: main processing node of cmri library - cmri.process(); - - // 2: update output. Reads bit 0 of T packet and sets the LED to this - digitalWrite(LED_PIN, cmri.get_bit(0)); - - // 3: update input. Flips a bit back and forth every second or so - cmri.set_bit(0, (millis() / 1000) % 2 == 0); -} +void loop() +{ + // 1: main processing node of cmri library + cmri.process(); + + // 2: update output. Reads bit 0 of T packet and sets the LED to this + digitalWrite(LED_PIN, cmri.get_bit(0)); + // 3: update input. Flips a bit back and forth every second or so + cmri.set_bit(0, (millis() / 1000) % 2 == 0); +} diff --git a/examples/susic_256_outputs/susic_256_outputs.ino b/examples/susic_256_outputs/susic_256_outputs.ino index 36808d4..ba4cfed 100644 --- a/examples/susic_256_outputs/susic_256_outputs.ino +++ b/examples/susic_256_outputs/susic_256_outputs.ino @@ -2,17 +2,19 @@ CMRI cmri(0, 0, 256); // address 0, 0 inputs, 256 outputs -void setup() { - Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI - pinMode(2, OUTPUT); - pinMode(3, OUTPUT); +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // make sure this matches your speed set in JMRI + pinMode(2, OUTPUT); + pinMode(3, OUTPUT); } -void loop() { - // 1: main processing node of cmri library - cmri.process(); - - // 2: update output. Reads bit 0 of T packet and sets the LED to this - digitalWrite(2, cmri.get_bit(0)); - digitalWrite(3, cmri.get_bit(255)); +void loop() +{ + // 1: main processing node of cmri library + cmri.process(); + + // 2: update output. Reads bit 0 of T packet and sets the LED to this + digitalWrite(2, cmri.get_bit(0)); + digitalWrite(3, cmri.get_bit(255)); } diff --git a/extras/hardware_test/README.md b/extras/hardware_test/README.md new file mode 100644 index 0000000..fde104d --- /dev/null +++ b/extras/hardware_test/README.md @@ -0,0 +1,20 @@ +# Test on Real Hardware + +This is a tiny sketch to test the current library code on a real device. It +emulates a default SMINI node against a JMRI host, mirroring output bit 0 on the +onboard LED and reporting pin 12 (with pull-up) as input bit 0. + +Build + upload + monitor: +``` +$ pio run -d extras/hardware_test -e uno -t upload +$ pio device monitor -b 9600 +``` + +Meaningfully exercising the protocol needs a JMRI host polling the node over a +C/MRI serial connection (9600 baud, SERIAL_8N2). 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..08ba45f --- /dev/null +++ b/extras/hardware_test/platformio.ini @@ -0,0 +1,26 @@ +; Hardware smoke-test for the CMRI 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/CMRI.* 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 protocol needs a JMRI host polling the node +; over serial; 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 = CMRI +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..7f98d74 --- /dev/null +++ b/extras/hardware_test/src/main.cpp @@ -0,0 +1,44 @@ +/* + Hardware smoke-test for CMRI. + + Emulates a default SMINI node (address 0, 24 inputs, 48 outputs) over the + hardware Serial port and mirrors the first output bit on the onboard LED: + + - digital pin 12 (with pull-up) is reported as input bit 0 + - output bit 0 (light CS0001 in JMRI) drives LED_BUILTIN + + Connect the board to a JMRI host configured for a C/MRI serial connection at + 9600 baud (SERIAL_8N2), then toggle light CS0001 to see the LED follow, and + ground pin 12 to see the sensor go active. Without a JMRI host 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 uint8_t INPUT_PIN = 12; + +CMRI cmri; // default SMINI: address 0, 24 inputs, 48 outputs, on Serial + +void setup() +{ + Serial.begin(9600, SERIAL_8N2); // must match the speed set in JMRI + pinMode(INPUT_PIN, INPUT_PULLUP); + pinMode(LED_BUILTIN, OUTPUT); +} + +void loop() +{ + // Process any pending serial traffic (auto-replies to POLLs). + cmri.process(); + + // Mirror output bit 0 on the LED. + digitalWrite(LED_BUILTIN, cmri.get_bit(0)); + + // Report pin 12 as input bit 0 (inverted: grounded == active). + cmri.set_bit(0, !digitalRead(INPUT_PIN)); +} diff --git a/keywords.txt b/keywords.txt index 06c75a9..6e59016 100644 --- a/keywords.txt +++ b/keywords.txt @@ -13,7 +13,6 @@ CMRI KEYWORD1 ####################################### set_address KEYWORD2 -set_length KEYWORD2 process KEYWORD2 process_char KEYWORD2 transmit KEYWORD2 @@ -21,9 +20,6 @@ get_bit KEYWORD2 get_byte KEYWORD2 set_bit KEYWORD2 set_byte KEYWORD2 -decode KEYWORD2 -valid KEYWORD2 -packet_type KEYWORD2 ####################################### # Constants (LITERAL1) diff --git a/library.json b/library.json index c74d22d..9b5dcab 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "CMRI", - "version": "1.5.1", + "version": "1.6.0", "description": "A library for interfacing Arduino with the C/MRI computer control system for model railroads. This library allows you to easily interface your Arduino with JMRI (Java Model Railroad Interface) by emulating Bruce Chubb's Computer/Model Railroad Interface (C/MRI) System. It provides a simple API to handle GET, SET, and POLL requests from JMRI automatically, with support for up to 2048 digital lines.", "keywords": "CMRI, JMRI, model-railroad, arduino, communication", "repository": { @@ -17,9 +17,5 @@ "license": "MIT", "frameworks": "arduino", "platforms": "*", - "headers": "CMRI.h", - "build": { - "srcDir": ".", - "includeDir": "." - } -} \ No newline at end of file + "headers": "CMRI.h" +} \ No newline at end of file diff --git a/library.properties b/library.properties index b5e0b8a..795f12b 100644 --- a/library.properties +++ b/library.properties @@ -1,7 +1,7 @@ name=CMRI -version=1.5.1 -author=Michael Adams -maintainer=Michael Adams +version=1.6.0 +author=Michael Adams +maintainer=Michael Adams sentence=A library for interfacing Arduino with the C/MRI computer control system for model railroads. paragraph=This library allows you to easily interface your Arduino with JMRI (Java Model Railroad Interface) by emulating Bruce Chubb's Computer/Model Railroad Interface (C/MRI) System. It provides a simple API to handle GET, SET, and POLL requests from JMRI automatically, with support for up to 2048 digital lines. category=Communication diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..44bb396 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,18 @@ +; PlatformIO project configuration for ArduinoCMRI. +; +; pio test -e native -> run the native unit tests (mocks the Arduino Stream) +; 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/src/CMRI.cpp b/src/CMRI.cpp new file mode 100644 index 0000000..6c91073 --- /dev/null +++ b/src/CMRI.cpp @@ -0,0 +1,252 @@ +/* + CMRI - a small library for Arduino to interface with the C/MRI + computer control system for model railroads + Copyright (C) 2012 Michael Adams (www.michael.net.nz) + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "CMRI.h" +#include + +CMRI::CMRI(unsigned int address, unsigned int input_bits, unsigned int output_bits, Stream &serial_class) + // store details + : _address(address), _rx_length((output_bits + 7) / 8), _tx_length((input_bits + 7) / 8) + + // set state + , + _rx_buffer((char *)malloc(_rx_length)), _tx_buffer((char *)malloc(_tx_length)) + + , + _serial(serial_class) + + // parsing state + , + _mode(PREAMBLE_1), _rx_index(0) + +{ + // clear to zero + for (int i = 0; i < _rx_length; i++) + _rx_buffer[i] = 0; + for (int i = 0; i < _tx_length; i++) + _tx_buffer[i] = 0; +} + +void CMRI::set_address(unsigned int address) +{ + _address = address; +} + +// reads in serial data, decodes packets +// automatically responds to POLL requests +// returns packet type so if we got a SET request you know to update your outputs +bool CMRI::process() +{ + while (_serial.available() > 0) + { + if (process_char(_serial.read())) + { + return true; + } + } + return false; +} + +bool CMRI::process_char(char c) +{ + // if it's a SET that's fine do nothing + // if it's an INIT that's also fine, we don't really care + // if it's a GET, well, do nothing since it must be someone else replying + // if it's a POLL then reply straight away with our data + switch (_decode(c)) + { + case POLL: + transmit(); + return true; + + case SET: + return true; + + default: + return false; + } +} + +// public methods + +bool CMRI::get_bit(int pos) +{ + // 1: divide index by 8 to get byte offset + char c = get_byte(pos / 8); + // 2: return bit at that location + return (bool)((c >> (pos % 8)) & 0x01); +} + +char CMRI::get_byte(int pos) +{ + if (pos >= _rx_length) + return 0; // out of bounds + else + return _rx_buffer[pos]; +} + +bool CMRI::set_bit(int pos, bool bit) +{ + if (pos / 8 >= _tx_length) + return false; // out of bounds + else + { + int index = pos / 8; + _tx_buffer[index] = bit + ? _tx_buffer[index] | 1 << pos % 8 // if bit=1, then OR it + : _tx_buffer[index] & ~(1 << pos % 8) // if bit=0, then NAND it + ; + return true; + } +} + +bool CMRI::set_byte(int pos, char b) +{ + if (pos >= _tx_length) + return false; // out of bounds + else + { + _tx_buffer[pos] = b; + return true; + } +} + +void CMRI::transmit() +{ + delayMicroseconds(50); // a minscule delay to let things recover + _serial.write(255); + _serial.write(255); + _serial.write(STX); + _serial.write(65 + _address); + _serial.write(GET); + for (int i = 0; i < _tx_length; i++) + { + if (_tx_buffer[i] == ETX) + _serial.write(ESC); // escape because this looks like an STX bit (very basic protocol) + if (_tx_buffer[i] == ESC) + _serial.write(ESC); // escape because this looks like an escape bit (very basic protocol) + _serial.write(_tx_buffer[i]); + } + _serial.write(ETX); + _serial.flush(); +} + +// Private methods +uint8_t CMRI::_decode(uint8_t c) +{ + switch (_mode) + { + case PREAMBLE_1: + _rx_index = 0; + if (c == 0xFF) + _mode = PREAMBLE_2; + break; + + case PREAMBLE_2: + if (c == 0xFF) + _mode = PREAMBLE_3; + else + _mode = PREAMBLE_1; + break; + + case PREAMBLE_3: + if (c == STX) + _mode = DECODE_ADDR; + else + _mode = PREAMBLE_1; + break; + + case DECODE_ADDR: + if (c == 'A' + _address) + _mode = DECODE_CMD; + else if (c >= 'A') + _mode = IGNORE_CMD; + else + _mode = PREAMBLE_1; + break; + + case DECODE_CMD: + if (c == SET) + _mode = DECODE_DATA; + else if (c == POLL) + goto POSTAMBLE_POLL; + else + _mode = POSTAMBLE_OTHER; + break; + + case IGNORE_CMD: + _mode = IGNORE_DATA; + break; + + case DECODE_DATA: + if (c == ESC) + _mode = DECODE_ESC_DATA; + else if (c == ETX) + goto POSTAMBLE_SET; + else if (_rx_index >= _rx_length) + { + } + else + _rx_buffer[_rx_index++] = c; + break; + + case DECODE_ESC_DATA: + if (_rx_index >= _rx_length) + { + } + else + _rx_buffer[_rx_index++] = c; + _mode = DECODE_DATA; + break; + + case IGNORE_DATA: + if (c == ESC) + _mode = IGNORE_ESC_DATA; + else if (c == ETX) + goto POSTAMBLE_IGNORE; + break; + + case IGNORE_ESC_DATA: + _mode = IGNORE_DATA; + break; + + case POSTAMBLE_OTHER: + _mode = PREAMBLE_1; + break; + } + return NOOP; + +POSTAMBLE_SET: + _mode = PREAMBLE_1; + return SET; + +POSTAMBLE_POLL: + _mode = PREAMBLE_1; + return POLL; + +POSTAMBLE_IGNORE: + _mode = PREAMBLE_1; + return NOOP; +} diff --git a/src/CMRI.h b/src/CMRI.h new file mode 100644 index 0000000..8991889 --- /dev/null +++ b/src/CMRI.h @@ -0,0 +1,95 @@ +/* + CMRI - a small library for Arduino to interface with the C/MRI + computer control system for model railroads + Copyright (C) 2012 Michael Adams (www.michael.net.nz) + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#ifndef CMRI_h +#define CMRI_h + +#define _CMRI_VERSION 1.6.0 // version of this library +#include + +class CMRI +{ + public: + CMRI(unsigned int address = 0, unsigned int input_bits = 24, unsigned int output_bits = 48, Stream &serial_class = Serial); + void set_address(unsigned int address); + + bool process(); + bool process_char(char c); + void transmit(); + + bool get_bit(int n); + char get_byte(int n); + + bool set_bit(int n, bool b); + bool set_byte(int n, char b); + + enum + { + MAX = 258, // max packet length in bytes (64 i/o cards @ 32 bits each + packet type and address bytes) + INIT = 'I', // PC is telling us stuff we don't really care about + SET = 'T', // as in TX from the PC => Arduino, PC is SETing our status + GET = 'R', // as in TX from Arduino => PC, PC is GETing our status + POLL = 'P', // PC wants to know our status + NOOP = 0x00, // do nothing + STX = 0x02, // start byte + ETX = 0x03, // end byte + ESC = 0x10, // escape byte + }; + + private: + enum + { + PREAMBLE_1, + PREAMBLE_2, + PREAMBLE_3, + DECODE_ADDR, + DECODE_CMD, + DECODE_DATA, + DECODE_ESC_DATA, + IGNORE_CMD, + IGNORE_DATA, + IGNORE_ESC_DATA, + POSTAMBLE_SET, + POSTAMBLE_POLL, + POSTAMBLE_OTHER + }; + + int _address; + int _rx_length; + int _tx_length; + char _rx_packet_type; + char *_rx_buffer; + char *_tx_buffer; + + Stream &_serial; + + // parsing state variables + int _mode; + int _rx_index; + + uint8_t _decode(uint8_t c); // process one character received from serial port +}; + +#endif diff --git a/test/mock/Arduino.h b/test/mock/Arduino.h new file mode 100644 index 0000000..b1d835b --- /dev/null +++ b/test/mock/Arduino.h @@ -0,0 +1,69 @@ +/* + Minimal Arduino.h mock for native unit tests. + + Provides just enough of the Arduino runtime for CMRI to build and run + off-device. The core of the library is a Stream-based protocol decoder, so + the mock supplies a concrete Stream backed by an input queue the test fills + and an output buffer the test inspects. +*/ + +#ifndef _CMRI_test_Arduino_h +#define _CMRI_test_Arduino_h + +#include +#include +#include // malloc/free used by the CMRI constructor +#include +#include + +// Serial config token; the mock never interprets it. +#define SERIAL_8N2 0x0E + +// A concrete stand-in for the Arduino Stream interface. rx holds bytes waiting +// to be read() by the library; tx collects everything the library write()s. +class Stream +{ + public: + std::deque rx; + std::vector tx; + + int available() + { + return (int)rx.size(); + } + + int read() + { + if (rx.empty()) + return -1; + uint8_t c = rx.front(); + rx.pop_front(); + return c; + } + + size_t write(uint8_t c) + { + tx.push_back(c); + return 1; + } + + void flush() + { + } + + // Test helper: enqueue a byte as if it had arrived over the wire. + void feed(uint8_t c) + { + rx.push_back(c); + } +}; + +inline void delayMicroseconds(unsigned int) +{ +} + +// The default argument of the CMRI constructor references Serial; the test +// translation unit defines it. +extern Stream Serial; + +#endif diff --git a/test/test_cmri/test_main.cpp b/test/test_cmri/test_main.cpp new file mode 100644 index 0000000..4261a86 --- /dev/null +++ b/test/test_cmri/test_main.cpp @@ -0,0 +1,208 @@ +/* + Native unit tests for CMRI. + + These run off-device via PlatformIO's `native` platform. The Arduino Stream + is mocked (see test/mock/Arduino.h) with a concrete class backed by an input + queue and an output buffer, so we can feed the decoder raw C/MRI packets and + inspect the frames the library transmits. + + Protocol reminder (all packets are wrapped FF FF STX ... ETX): + - the address byte is 'A' + node address + - SET ('T') carries output data PC -> node + - POLL ('P') asks the node to transmit its input state back + - GET ('R') is the frame the node sends in reply to a POLL + In CMRI terms, set_bit/set_byte stage the node's *input* state (sent on + transmit); get_bit/get_byte read the *output* state received in a SET. +*/ + +#include + +#include "Arduino.h" +#include "CMRI.h" + +// Referenced by the CMRI constructor's default argument (unused by the tests, +// which always pass an explicit Stream, but needed to satisfy the symbol). +Stream Serial; + +void setUp(void) +{ +} + +void tearDown(void) +{ +} + +// --- helpers --------------------------------------------------------------- + +// Feed a full framed packet (preamble + STX + body + ETX) into the stream. +static void feed_packet(Stream &s, uint8_t addr, uint8_t cmd, const uint8_t *data, size_t len) +{ + s.feed(0xFF); + s.feed(0xFF); + s.feed(CMRI::STX); + s.feed('A' + addr); + s.feed(cmd); + for (size_t i = 0; i < len; i++) + s.feed(data[i]); + s.feed(CMRI::ETX); +} + +// --- bit/byte accessors ---------------------------------------------------- + +// A staged input bit shows up in the transmitted GET frame. +void test_set_bit_reflected_in_transmit(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); // 3 input bytes, 6 output bytes + + TEST_ASSERT_TRUE(cmri.set_bit(0, true)); + TEST_ASSERT_TRUE(cmri.set_bit(9, true)); // byte 1, bit 1 + + cmri.transmit(); + + // Frame: FF FF STX 'A' GET <3 data bytes> ETX + TEST_ASSERT_EQUAL_UINT8(0x01, s.tx[5]); // byte 0, bit 0 + TEST_ASSERT_EQUAL_UINT8(0x02, s.tx[6]); // byte 1, bit 1 + TEST_ASSERT_EQUAL_UINT8(0x00, s.tx[7]); +} + +// Regression for the set_bit() bounds bug: the old (pos + 7) / 8 check wrongly +// rejected the high bits (17..23) of the last input byte on a default SMINI. +void test_set_bit_accepts_high_bits_of_last_byte(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); // 3 input bytes -> valid bits 0..23 + + for (int pos = 17; pos <= 23; pos++) + TEST_ASSERT_TRUE(cmri.set_bit(pos, true)); + + cmri.transmit(); + TEST_ASSERT_EQUAL_UINT8(0xFE, s.tx[7]); // bits 1..7 of byte 2 set +} + +// Bits past the configured input width are rejected. +void test_set_bit_out_of_bounds(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); // valid bits 0..23 + TEST_ASSERT_FALSE(cmri.set_bit(24, true)); + TEST_ASSERT_FALSE(cmri.set_bit(100, true)); +} + +// set_byte respects the input length; get_byte returns 0 past the output length. +void test_byte_bounds(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); // 3 input bytes, 6 output bytes + TEST_ASSERT_TRUE(cmri.set_byte(2, 0xAB)); + TEST_ASSERT_FALSE(cmri.set_byte(3, 0xAB)); + TEST_ASSERT_EQUAL_UINT8(0, cmri.get_byte(6)); // past output length +} + +// --- protocol behaviour ---------------------------------------------------- + +// A POLL for our address makes process() transmit a well-formed GET frame. +void test_poll_produces_get_frame(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); + + cmri.set_byte(0, 0x5A); // stage some input state + + feed_packet(s, 0, CMRI::POLL, nullptr, 0); + TEST_ASSERT_TRUE(cmri.process()); + + const std::vector &tx = s.tx; + TEST_ASSERT_EQUAL_UINT(9u, tx.size()); // 2 preamble + STX + addr + GET + 3 data + ETX + TEST_ASSERT_EQUAL_UINT8(0xFF, tx[0]); + TEST_ASSERT_EQUAL_UINT8(0xFF, tx[1]); + TEST_ASSERT_EQUAL_UINT8(CMRI::STX, tx[2]); + TEST_ASSERT_EQUAL_UINT8('A' + 0, tx[3]); + TEST_ASSERT_EQUAL_UINT8(CMRI::GET, tx[4]); + TEST_ASSERT_EQUAL_UINT8(0x5A, tx[5]); + TEST_ASSERT_EQUAL_UINT8(CMRI::ETX, tx[8]); +} + +// A SET updates the output buffer read back through get_bit/get_byte. +void test_set_packet_updates_outputs(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); + + uint8_t data[6] = {0x01, 0x00, 0x80, 0, 0, 0}; + feed_packet(s, 0, CMRI::SET, data, 6); + + TEST_ASSERT_TRUE(cmri.process()); + TEST_ASSERT_TRUE(cmri.get_bit(0)); // byte 0, bit 0 + TEST_ASSERT_FALSE(cmri.get_bit(1)); + TEST_ASSERT_TRUE(cmri.get_bit(23)); // byte 2, bit 7 + TEST_ASSERT_EQUAL_UINT8(0x80, cmri.get_byte(2)); +} + +// A packet addressed to another node is ignored: process() is false and no +// outputs change. +void test_address_filtering(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); // we are node 0 + + uint8_t data[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + feed_packet(s, 1, CMRI::SET, data, 6); // addressed to node 1 + + TEST_ASSERT_FALSE(cmri.process()); + TEST_ASSERT_FALSE(cmri.get_bit(0)); + TEST_ASSERT_EQUAL_UINT8(0, cmri.get_byte(0)); +} + +// Data bytes that collide with ETX/ESC are escaped in the transmitted frame. +void test_transmit_escapes_control_bytes(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); + + cmri.set_byte(0, CMRI::ETX); // looks like end-of-frame + cmri.set_byte(1, CMRI::ESC); // looks like an escape + + cmri.transmit(); + + // header (5) then escaped payload then ETX + TEST_ASSERT_EQUAL_UINT8(CMRI::ESC, s.tx[5]); + TEST_ASSERT_EQUAL_UINT8(CMRI::ETX, s.tx[6]); + TEST_ASSERT_EQUAL_UINT8(CMRI::ESC, s.tx[7]); + TEST_ASSERT_EQUAL_UINT8(CMRI::ESC, s.tx[8]); + TEST_ASSERT_EQUAL_UINT8(0x00, s.tx[9]); + TEST_ASSERT_EQUAL_UINT8(CMRI::ETX, s.tx[10]); +} + +// Garbage before a valid packet is resynced away by the preamble state machine. +void test_preamble_resync_after_garbage(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); + + // Junk, including a lone 0xFF that must not be mistaken for the preamble. + s.feed(0x00); + s.feed(0xFF); + s.feed(0x13); + s.feed(0x41); + + feed_packet(s, 0, CMRI::POLL, nullptr, 0); + TEST_ASSERT_TRUE(cmri.process()); + TEST_ASSERT_EQUAL_UINT(9u, s.tx.size()); + TEST_ASSERT_EQUAL_UINT8(CMRI::GET, s.tx[4]); +} + +int main(int, char **) +{ + UNITY_BEGIN(); + RUN_TEST(test_set_bit_reflected_in_transmit); + RUN_TEST(test_set_bit_accepts_high_bits_of_last_byte); + RUN_TEST(test_set_bit_out_of_bounds); + RUN_TEST(test_byte_bounds); + RUN_TEST(test_poll_produces_get_frame); + RUN_TEST(test_set_packet_updates_outputs); + RUN_TEST(test_address_filtering); + RUN_TEST(test_transmit_escapes_control_bytes); + RUN_TEST(test_preamble_resync_after_garbage); + return UNITY_END(); +}