Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow rewinding of commands sent via UART buffer #1346

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions FluidNC/src/FixedCircularBuffer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2024 - Dylan Knutson <[email protected]>
// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file.

#pragma once

#include <vector>
#include <optional>

/**
* 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 <typename T>
class FixedCircularBuffer {
public:
std::vector<T> 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<T> 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; }
};
8 changes: 5 additions & 3 deletions FluidNC/src/Flowcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,10 @@ Error flowcontrol(uint32_t o_label, char* line, size_t& pos, bool& skip) {
case Op_Repeat:
if (Job::active()) {
if (!skipping && (status = expression(line, pos, value)) == Error::Ok) {
stack_push(o_label, operation, !value);
if (value) {
// TODO - return an error if value < 0
// For now, just guard against negative values
stack_push(o_label, operation, !(value > 0.0));
if (value > 0.0) {
context.top().file = Job::source();
context.top().file_pos = context.top().file->position();
context.top().repeats = (uint32_t)value;
Expand All @@ -249,7 +251,7 @@ Error flowcontrol(uint32_t o_label, char* line, size_t& pos, bool& skip) {
if (Job::active()) {
if (last_op == Op_Repeat) {
if (o_label == context.top().o_label) {
if (context.top().repeats && --context.top().repeats) {
if (context.top().repeats && --context.top().repeats > 0.0) {
context.top().file->set_position(context.top().file_pos);
} else {
stack_pull();
Expand Down
32 changes: 23 additions & 9 deletions FluidNC/src/Job.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@
#include "Job.h"
#include <map>
#include <stack>
#include "Protocol.h"

std::stack<JobSource*> 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
Expand All @@ -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;
}
Expand All @@ -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();
}
1 change: 1 addition & 0 deletions FluidNC/src/Job.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
};
Expand Down
2 changes: 2 additions & 0 deletions FluidNC/src/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ extern volatile bool rtCycleStop;

extern volatile bool runLimitLoop;

extern Channel* activeChannel;

// Alarm codes.
enum class ExecAlarm : uint8_t {
None = 0,
Expand Down
21 changes: 18 additions & 3 deletions FluidNC/src/UartChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>(512);
_history_buffer_pos = 0;
}

void UartChannel::init() {
Expand Down Expand Up @@ -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();
}

Expand All @@ -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;
}

Expand Down
10 changes: 8 additions & 2 deletions FluidNC/src/UartChannel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<char> _history_buffer;
std::size_t _history_buffer_pos;

int _uart_num = 0;
int _report_interval_ms = 0;
Expand Down Expand Up @@ -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;
Expand Down
42 changes: 42 additions & 0 deletions FluidNC/tests/FixedCircularBufferTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2024 - Dylan Knutson <[email protected]>
// 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<int> 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<int> 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<int> 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);
}
45 changes: 45 additions & 0 deletions fixture_tests/fixtures/flow_control_repeat.nc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
ignore ok

# test repeat zero times (should not print anything)
-> o100 repeat [0]
-> (print, fail lit 0)
-> o100 endrepeat
-> (print, pass lit 0)
<- [MSG:INFO: PRINT, pass lit 0]

# test when using a variable
-> #<count> = 0
-> o100 repeat [#<count>]
-> (print, fail var 0)
-> o100 endrepeat
-> (print, pass var 0)
<- [MSG:INFO: PRINT, pass var 0]

# test negative repeat (should not print anything)
# todo - negative repeat should probably set an error
-> o100 repeat [-1]
-> (print, fail lit -1)
-> o100 endrepeat
-> (print, pass lit -1)
<- [MSG:INFO: PRINT, pass lit -1]

# test repeat a fixed number of times
-> #<count> = 0
-> o100 repeat [3]
-> #<count> = [#<count> + 1]
-> (print, count=%d#<count>)
<- [MSG:INFO: PRINT, count=1]
-> o100 endrepeat
<- [MSG:INFO: PRINT, count=2]
<- [MSG:INFO: PRINT, count=3]

# test repeating a variable number of times
-> #<count> = 0
-> #<i> = 3
-> o100 repeat [#<i>]
-> #<count> = [#<count> + 1]
-> (print, count=%d#<count>)
<- [MSG:INFO: PRINT, count=1]
-> o100 endrepeat
<- [MSG:INFO: PRINT, count=2]
<- [MSG:INFO: PRINT, count=3]
9 changes: 8 additions & 1 deletion fixture_tests/run_fixture
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ else:
def run_fixture(fixture_path, controller):
op_entries_parsed = op_entries.parse_file(fixture_path)

print(
colored(f"--- Run fixture ", "blue")
+ colored(fixture_path, "blue", attrs=["bold"])
+ colored(" ---", "blue")
)

try:
for op_entry in op_entries_parsed:
if not op_entry.execute(controller):
Expand All @@ -39,7 +45,8 @@ def run_fixture(fixture_path, controller):
exit(1)

except KeyboardInterrupt:
print("Interrupt")
print("Interrupted by user")
exit(1)

except TimeoutError as e:
print("Timeout waiting for response, line: " + e.args[0])
Expand Down
Loading
Loading