From 9b721a0a2950640a0847441644bbe83edf60212a Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Wed, 25 Sep 2024 13:48:55 -0700 Subject: [PATCH] [WIP] Allow looping uart streamed gcode --- FluidNC/src/FixedCircularBuffer.h | 61 +++++++++++++++++++++++ FluidNC/src/Job.cpp | 32 ++++++++---- FluidNC/src/Job.h | 1 + FluidNC/src/Protocol.h | 2 + FluidNC/src/UartChannel.cpp | 21 ++++++-- FluidNC/src/UartChannel.h | 10 +++- FluidNC/src/testbuffer.cpp.bak | 59 ++++++++++++++++++++++ FluidNC/tests/FixedCircularBufferTest.cpp | 42 ++++++++++++++++ 8 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 FluidNC/src/FixedCircularBuffer.h create mode 100644 FluidNC/src/testbuffer.cpp.bak create mode 100644 FluidNC/tests/FixedCircularBufferTest.cpp diff --git a/FluidNC/src/FixedCircularBuffer.h b/FluidNC/src/FixedCircularBuffer.h new file mode 100644 index 000000000..f2936b15a --- /dev/null +++ b/FluidNC/src/FixedCircularBuffer.h @@ -0,0 +1,61 @@ +// Copyright (c) 2024 - Dylan Knutson +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include +#include + +/** + * A fixed-size circular buffer that stores elements of type T. + * Keeps track of how many elements have been pushed onto it, and allows + * for indexing as if it was an infinite sized array. If indexing into + * the buffer would result in an out-of-bounds access, returns std::nullopt. + * + * This is useful for implementing "scrollback" of a buffer of e.g. user + * provided commands, without using an unbounded amount of memory. + */ +template +class FixedCircularBuffer { +public: + std::vector storage; + std::size_t head_idx, tail_idx; + +public: + FixedCircularBuffer() : FixedCircularBuffer(0) {} + FixedCircularBuffer(size_t size) : storage(size), head_idx(0), tail_idx(0) {} + + /** + * Push an element onto the end of the buffer. + */ + void push(T&& elem) { + storage[tail_idx % storage.size()] = std::move(elem); + tail_idx += 1; + if (tail_idx - head_idx > storage.size()) { + head_idx += 1; + } + } + + /** + * Get the element at the given index, or std::nullopt if the index is out of bounds. + */ + std::optional at(std::size_t idx) const { + if (idx >= tail_idx) { + return std::nullopt; + } + if (idx < head_idx) { + return std::nullopt; + } + return storage[idx % storage.size()]; + } + + /** + * Is the buffer empty? + */ + bool is_empty() const { return head_idx == tail_idx; } + + /** + * Get the index of the last element pushed onto the buffer. + */ + std::size_t position() const { return tail_idx; } +}; diff --git a/FluidNC/src/Job.cpp b/FluidNC/src/Job.cpp index 5de670121..a97801d08 100644 --- a/FluidNC/src/Job.cpp +++ b/FluidNC/src/Job.cpp @@ -4,17 +4,29 @@ #include "Job.h" #include #include +#include "Protocol.h" std::stack job; Channel* Job::leader = nullptr; bool Job::active() { - return !job.empty(); + return !job.empty() || activeChannel; } +JobSource activeChannelJobSource(nullptr); + JobSource* Job::source() { - return job.empty() ? nullptr : job.top(); + if (job.empty()) { + if (activeChannel) { + activeChannelJobSource.set_channel(activeChannel); + return &activeChannelJobSource; + } else { + return nullptr; + } + } else { + return job.top(); + } } // save() and restore() are use to close/reopen an SD file atop the job stack @@ -38,9 +50,11 @@ void Job::nest(Channel* in_channel, Channel* out_channel) { job.push(source); } void Job::pop() { - auto source = job.top(); - job.pop(); - delete source; + if (!job.empty()) { + auto source = job.top(); + job.pop(); + delete source; + } if (!active()) { leader = nullptr; } @@ -60,14 +74,14 @@ void Job::abort() { } bool Job::get_param(const std::string& name, float& value) { - return job.top()->get_param(name, value); + return source()->get_param(name, value); } bool Job::set_param(const std::string& name, float value) { - return job.top()->set_param(name, value); + return source()->set_param(name, value); } bool Job::param_exists(const std::string& name) { - return job.top()->param_exists(name); + return source()->param_exists(name); } Channel* Job::channel() { - return job.top()->channel(); + return source()->channel(); } diff --git a/FluidNC/src/Job.h b/FluidNC/src/Job.h index c44684b77..378b659b5 100644 --- a/FluidNC/src/Job.h +++ b/FluidNC/src/Job.h @@ -31,6 +31,7 @@ class JobSource { void set_position(size_t pos) { _channel->set_position(pos); } Channel* channel() { return _channel; } + void set_channel(Channel* channel) { _channel = channel; } ~JobSource() { delete _channel; } }; diff --git a/FluidNC/src/Protocol.h b/FluidNC/src/Protocol.h index 37d08c4d7..0ede1bc23 100644 --- a/FluidNC/src/Protocol.h +++ b/FluidNC/src/Protocol.h @@ -50,6 +50,8 @@ extern volatile bool rtCycleStop; extern volatile bool runLimitLoop; +extern Channel* activeChannel; + // Alarm codes. enum class ExecAlarm : uint8_t { None = 0, diff --git a/FluidNC/src/UartChannel.cpp b/FluidNC/src/UartChannel.cpp index 9859f78ae..1c907d327 100644 --- a/FluidNC/src/UartChannel.cpp +++ b/FluidNC/src/UartChannel.cpp @@ -6,8 +6,10 @@ #include "Serial.h" // allChannels UartChannel::UartChannel(int num, bool addCR) : Channel("uart_channel", num, addCR) { - _lineedit = new Lineedit(this, _line, Channel::maxLine - 1); - _active = false; + _lineedit = new Lineedit(this, _line, Channel::maxLine - 1); + _active = false; + _history_buffer = FixedCircularBuffer(512); + _history_buffer_pos = 0; } void UartChannel::init() { @@ -63,10 +65,13 @@ size_t UartChannel::write(const uint8_t* buffer, size_t length) { } int UartChannel::available() { - return _uart->available(); + return (_history_buffer_pos < _history_buffer.position()) || _uart->available(); } int UartChannel::peek() { + if (_history_buffer_pos < _history_buffer.position()) { + return _history_buffer.at(_history_buffer_pos).value(); + } return _uart->peek(); } @@ -90,12 +95,22 @@ bool UartChannel::lineComplete(char* line, char c) { } int UartChannel::read() { + if (_history_buffer_pos < _history_buffer.position()) { + int c = _history_buffer.at(_history_buffer_pos).value(); + _history_buffer_pos += 1; + return c; + } + int c = _uart->read(); if (c == 0x11) { // 0x11 is XON. If we receive that, it is a request to use software flow control _uart->setSwFlowControl(true, -1, -1); return -1; } + if (c != -1) { + _history_buffer.push((char)c); + _history_buffer_pos += 1; + } return c; } diff --git a/FluidNC/src/UartChannel.h b/FluidNC/src/UartChannel.h index eb8cbb955..1c2ffbfff 100644 --- a/FluidNC/src/UartChannel.h +++ b/FluidNC/src/UartChannel.h @@ -6,11 +6,14 @@ #include "Uart.h" #include "Channel.h" #include "lineedit.h" +#include "FixedCircularBuffer.h" class UartChannel : public Channel, public Configuration::Configurable { private: - Lineedit* _lineedit; - Uart* _uart; + Lineedit* _lineedit; + Uart* _uart; + FixedCircularBuffer _history_buffer; + std::size_t _history_buffer_pos; int _uart_num = 0; int _report_interval_ms = 0; @@ -49,6 +52,9 @@ class UartChannel : public Channel, public Configuration::Configurable { handler.item("uart_num", _uart_num); handler.item("message_level", _message_level, messageLevels2); } + + size_t position() override { return _history_buffer_pos; } + void set_position(size_t pos) override { _history_buffer_pos = pos; } }; extern UartChannel Uart0; diff --git a/FluidNC/src/testbuffer.cpp.bak b/FluidNC/src/testbuffer.cpp.bak new file mode 100644 index 000000000..b6fc1f323 --- /dev/null +++ b/FluidNC/src/testbuffer.cpp.bak @@ -0,0 +1,59 @@ +#include "FixedCircularBuffer.h" + +#include +#include + +template +std::ostream& operator<<(std::ostream& os, std::vector vec) { + os << "{ "; + std::copy(vec.begin(), vec.end(), std::ostream_iterator(os, " ")); + os << "}"; + return os; +} + +template +std::ostream& operator<<(std::ostream& os, FixedCircularBuffer buff) { + os << "[head_idx: " << buff.head_idx << "][tail_idx: " << buff.tail_idx << "][storage: " << buff.storage << "]"; + return os; +} + +template +std::ostream& operator<<(std::ostream& os, std::optional opt) { + if (opt.has_value()) { + os << opt.value(); + } else { + os << "nullopt"; + } + return os; +} + +int main() { + FixedCircularBuffer buffer(4); + buffer.push('a'); + buffer.push('b'); + std::cout << "buffer is " << buffer << std::endl; + std::cout << "buffer.at(0) is " << buffer.at(0) << std::endl; + std::cout << "buffer.at(1) is " << buffer.at(1) << std::endl; + std::cout << "buffer.at(2) is " << buffer.at(2) << std::endl; + std::cout << "buffer.at(3) is " << buffer.at(3) << std::endl; + std::cout << std::endl; + + buffer.push('c'); + buffer.push('d'); + std::cout << "buffer is " << buffer << std::endl; + std::cout << "buffer.at(0) is " << buffer.at(0) << std::endl; + std::cout << "buffer.at(1) is " << buffer.at(1) << std::endl; + std::cout << "buffer.at(2) is " << buffer.at(2) << std::endl; + std::cout << "buffer.at(3) is " << buffer.at(3) << std::endl; + std::cout << std::endl; + + buffer.push('e'); + std::cout << "buffer is " << buffer << std::endl; + std::cout << "buffer.at(0) is " << buffer.at(0) << std::endl; + std::cout << "buffer.at(1) is " << buffer.at(1) << std::endl; + std::cout << "buffer.at(2) is " << buffer.at(2) << std::endl; + std::cout << "buffer.at(3) is " << buffer.at(3) << std::endl; + std::cout << "buffer.at(4) is " << buffer.at(4) << std::endl; + std::cout << "buffer.at(5) is " << buffer.at(5) << std::endl; + std::cout << std::endl; +} diff --git a/FluidNC/tests/FixedCircularBufferTest.cpp b/FluidNC/tests/FixedCircularBufferTest.cpp new file mode 100644 index 000000000..729f1ce13 --- /dev/null +++ b/FluidNC/tests/FixedCircularBufferTest.cpp @@ -0,0 +1,42 @@ +// Copyright (c) 2024 - Dylan Knutson +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#include "gtest/gtest.h" +#include "src/FixedCircularBuffer.h" + +TEST(FixedCircularBuffer, Empty) { + FixedCircularBuffer buffer(0); + + ASSERT_TRUE(buffer.is_empty()); + ASSERT_EQ(buffer.position(), 0); + ASSERT_EQ(buffer.at(0), std::nullopt); + ASSERT_EQ(buffer.at(1), std::nullopt); + ASSERT_EQ(buffer.at(2), std::nullopt); +} + +TEST(FixedCircularBuffer, OneElement) { + FixedCircularBuffer buffer(1); + + buffer.push(42); + + ASSERT_FALSE(buffer.is_empty()); + ASSERT_EQ(buffer.position(), 1); + ASSERT_EQ(buffer.at(0), 42); + ASSERT_EQ(buffer.at(1), std::nullopt); + ASSERT_EQ(buffer.at(2), std::nullopt); +} + +TEST(FixedCircularBuffer, FrontElementsPopped) { + FixedCircularBuffer buffer(2); + + buffer.push(1); + buffer.push(2); + buffer.push(3); + + ASSERT_FALSE(buffer.is_empty()); + ASSERT_EQ(buffer.position(), 3); + ASSERT_EQ(buffer.at(0), std::nullopt); + ASSERT_EQ(buffer.at(1), 2); + ASSERT_EQ(buffer.at(2), 3); + ASSERT_EQ(buffer.at(3), std::nullopt); +}