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

Add support for per-serial feature querying #21318

7 changes: 4 additions & 3 deletions Marlin/src/core/bug_on.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@
// Useful macro for stopping the CPU on an unexpected condition
// This is used like SERIAL_ECHOPAIR, that is: a key-value call of the local variables you want
// to dump to the serial port before stopping the CPU.
#define BUG_ON(V...) do { SERIAL_ECHOPAIR(ONLY_FILENAME, __LINE__, ": "); SERIAL_ECHOLNPAIR(V); SERIAL_FLUSHTX(); *(char*)0 = 42; } while(0)
// \/ Don't replace by SERIAL_ECHOPAIR since ONLY_FILENAME cannot be transformed to a PGM string on Arduino and it breaks building
#define BUG_ON(V...) do { SERIAL_ECHO(ONLY_FILENAME); SERIAL_ECHO(__LINE__); SERIAL_ECHOLN(": "); SERIAL_ECHOLNPAIR(V); SERIAL_FLUSHTX(); *(char*)0 = 42; } while(0)
#elif ENABLED(MARLIN_DEV_MODE)
// Don't stop the CPU here, but at least dump the bug on the serial port
//#define BUG_ON(V...) do { SERIAL_ECHOPAIR(ONLY_FILENAME, __LINE__, ": BUG!\n"); SERIAL_ECHOLNPAIR(V); SERIAL_FLUSHTX(); } while(0)
#define BUG_ON(V...) NOOP
// \/ Don't replace by SERIAL_ECHOPAIR since ONLY_FILENAME cannot be transformed to a PGM string on Arduino and it breaks building
#define BUG_ON(V...) do { SERIAL_ECHO(ONLY_FILENAME); SERIAL_ECHO(__LINE__); SERIAL_ECHOLN(": BUG!"); SERIAL_ECHOLNPAIR(V); SERIAL_FLUSHTX(); } while(0)
#else
// Release mode, let's ignore the bug
#define BUG_ON(V...) NOOP
Expand Down
44 changes: 37 additions & 7 deletions Marlin/src/core/macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,16 @@

#endif

// Allow manipulating enumeration value like flags without ugly cast everywhere
#define ENUM_FLAGS(T) \
FORCE_INLINE constexpr T operator&(T x, T y) { return static_cast<T>(static_cast<int>(x) & static_cast<int>(y)); } \
FORCE_INLINE constexpr T operator|(T x, T y) { return static_cast<T>(static_cast<int>(x) | static_cast<int>(y)); } \
FORCE_INLINE constexpr T operator^(T x, T y) { return static_cast<T>(static_cast<int>(x) ^ static_cast<int>(y)); } \
FORCE_INLINE constexpr T operator~(T x) { return static_cast<T>(~static_cast<int>(x)); } \
FORCE_INLINE T & operator&=(T &x, T y) { return x &= y; } \
FORCE_INLINE T & operator|=(T &x, T y) { return x |= y; } \
FORCE_INLINE T & operator^=(T &x, T y) { return x ^= y; }

// C++11 solution that is standard compliant. <type_traits> is not available on all platform
namespace Private {
template<bool, typename _Tp = void> struct enable_if { };
Expand Down Expand Up @@ -357,23 +367,43 @@
return *str ? findStringEnd(str + 1) : str;
}

// Check whether a string contains a slash
constexpr bool containsSlash(const char *str) {
return *str == '/' ? true : (*str ? containsSlash(str + 1) : false);
// Check whether a string contains a specific character
constexpr bool contains(const char *str, const char ch) {
return *str == ch ? true : (*str ? contains(str + 1, ch) : false);
}
// Find the last position of the slash
constexpr const char* findLastSlashPos(const char *str) {
return *str == '/' ? (str + 1) : findLastSlashPos(str - 1);
// Find the last position of the specific character (should be called with findStringEnd)
constexpr const char* findLastPos(const char *str, const char ch) {
return *str == ch ? (str + 1) : findLastPos(str - 1, ch);
}
// Compile-time evaluation of the last part of a file path
// Typically used to shorten the path to file in compiled strings
// CompileTimeString::baseName(__FILE__) returns "macros.h" and not /path/to/Marlin/src/core/macros.h
constexpr const char* baseName(const char *str) {
return containsSlash(str) ? findLastSlashPos(findStringEnd(str)) : str;
return contains(str, '/') ? findLastPos(findStringEnd(str), '/') : str;
}

// Find the first occurence of a character in a string (or return the last position in the string)
constexpr const char* findFirst(const char *str, const char ch) {
return *str == ch || *str == 0 ? (str + 1) : findFirst(str + 1, ch);
}
// Compute the string length at compile time
constexpr unsigned stringLen(const char *str) {
return *str == 0 ? 0 : 1 + stringLen(str + 1);
}
}

#define ONLY_FILENAME CompileTimeString::baseName(__FILE__)
/** Get the templated type name. This does not depends on RTTI, but on the preprocessor, so it should be quite safe to use even on old compilers.
WARNING: DO NOT RENAME THIS FUNCTION (or change the text inside the function to match what the preprocessor will generate)
The name is chosen very short since the binary will store "const char* gtn(T*) [with T = YourTypeHere]" so avoid long function name here */
template <typename T>
inline const char* gtn(T*) {
// It works on GCC by instantiating __PRETTY_FUNCTION__ and parsing the result. So the syntax here is very limited to GCC output
constexpr unsigned verboseChatLen = sizeof("const char* gtn(T*) [with T = ") - 1;
static char templateType[sizeof(__PRETTY_FUNCTION__) - verboseChatLen] = {};
__builtin_memcpy(templateType, __PRETTY_FUNCTION__ + verboseChatLen, sizeof(__PRETTY_FUNCTION__) - verboseChatLen - 2);
return templateType;
}

#else

Expand Down
2 changes: 1 addition & 1 deletion Marlin/src/core/serial.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ inline void SERIAL_ECHO(serial_char_t x) { SERIAL_IMPL.write(x.c); }
#define AS_CHAR(C) serial_char_t(C)

// SERIAL_ECHO_F prints a floating point value with optional precision
inline void SERIAL_ECHO_F(EnsureDouble x, int digit = 2) { SERIAL_IMPL.print(x, digit); }
inline void SERIAL_ECHO_F(EnsureDouble x, int digit=2) { SERIAL_IMPL.print(x, digit); }

template <typename T>
void SERIAL_ECHOLN(T x) { SERIAL_IMPL.println(x); }
Expand Down
68 changes: 48 additions & 20 deletions Marlin/src/core/serial_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ struct serial_index_t {
constexpr serial_index_t() : index(-1) {}
};

// flushTX is not implemented in all HAL, so use SFINAE to call the method where it is.
CALL_IF_EXISTS_IMPL(void, flushTX);
CALL_IF_EXISTS_IMPL(bool, connected, true);

// In order to catch usage errors in code, we make the base to encode number explicit
// If given a number (and not this enum), the compiler will reject the overload, falling back to the (double, digit) version
// We don't want hidden conversion of the first parameter to double, so it has to be as hard to do for the compiler as creating this enum
Expand All @@ -59,19 +55,34 @@ enum class PrintBase {
Bin = 2
};

// A simple forward struct that prevent the compiler to select print(double, int) as a default overload for any type different than
// double or float. For double or float, a conversion exists so the call will be transparent
// A simple feature list enumeration
enum class SerialFeature {
None = 0x00,
MeatPack = 0x01, //!< Enabled when Meatpack is present
BinaryFileTransfer = 0x02, //!< Enabled for BinaryFile transfer support (in the future)
Virtual = 0x04, //!< Enabled for virtual serial port (like Telnet / Websocket / ...)
Hookable = 0x08, //!< Enabled if the serial class supports a setHook method
};
ENUM_FLAGS(SerialFeature);

// flushTX is not implemented in all HAL, so use SFINAE to call the method where it is.
CALL_IF_EXISTS_IMPL(void, flushTX);
CALL_IF_EXISTS_IMPL(bool, connected, true);
CALL_IF_EXISTS_IMPL(SerialFeature, features, SerialFeature::None);

// A simple forward struct to prevent the compiler from selecting print(double, int) as a default overload
// for any type other than double/float. For double/float, a conversion exists so the call will be invisible.
struct EnsureDouble {
double a;
FORCE_INLINE operator double() { return a; }
// If the compiler breaks on ambiguity here, it's likely because you're calling print(X, base) with X not a double or a float, and a
// base that's not one of PrintBase's value. This exact code is made to detect such error, you NEED to set a base explicitely like this:
// If the compiler breaks on ambiguity here, it's likely because print(X, base) is called with X not a double/float, and
// a base that's not a PrintBase value. This code is made to detect the error. You MUST set a base explicitly like this:
// SERIAL_PRINT(v, PrintBase::Hex)
FORCE_INLINE EnsureDouble(double a) : a(a) {}
FORCE_INLINE EnsureDouble(float a) : a(a) {}
};

// Using Curiously Recurring Template Pattern here to avoid virtual table cost when compiling.
// Using Curiously-Recurring Template Pattern here to avoid virtual table cost when compiling.
// Since the real serial class is known at compile time, this results in the compiler writing
// a completely efficient code.
template <class Child>
Expand All @@ -85,27 +96,44 @@ struct SerialBase {
SerialBase(const bool) {}
#endif

#define SerialChild static_cast<Child*>(this)

// Static dispatch methods below:
// The most important method here is where it all ends to:
size_t write(uint8_t c) { return static_cast<Child*>(this)->write(c); }
size_t write(uint8_t c) { return SerialChild->write(c); }

// Called when the parser finished processing an instruction, usually build to nothing
void msgDone() { static_cast<Child*>(this)->msgDone(); }
// Called upon initialization
void begin(const long baudRate) { static_cast<Child*>(this)->begin(baudRate); }
// Called upon destruction
void end() { static_cast<Child*>(this)->end(); }
void msgDone() const { SerialChild->msgDone(); }

// Called on initialization
void begin(const long baudRate) { SerialChild->begin(baudRate); }

// Called on destruction
void end() { SerialChild->end(); }

/** Check for available data from the port
@param index The port index, usually 0 */
int available(serial_index_t index = 0) { return static_cast<Child*>(this)->available(index); }
int available(serial_index_t index=0) const { return SerialChild->available(index); }

/** Read a value from the port
@param index The port index, usually 0 */
int read(serial_index_t index = 0) { return static_cast<Child*>(this)->read(index); }
int read(serial_index_t index=0) { return SerialChild->read(index); }

/** Combine the features of this serial instance and return it
@param index The port index, usually 0 */
SerialFeature features(serial_index_t index=0) const { return static_cast<const Child*>(this)->features(index); }

// Check if the serial port has a feature
bool has_feature(serial_index_t index, SerialFeature flag) const { (features(index) & flag) != SerialFeature::None; }

// Check if the serial port is connected (usually bypassed)
bool connected() { return static_cast<Child*>(this)->connected(); }
bool connected() const { return SerialChild->connected(); }

// Redirect flush
void flush() { static_cast<Child*>(this)->flush(); }
void flush() { SerialChild->flush(); }

// Not all implementation have a flushTX, so let's call them only if the child has the implementation
void flushTX() { CALL_IF_EXISTS(void, static_cast<Child*>(this), flushTX); }
void flushTX() { CALL_IF_EXISTS(void, SerialChild, flushTX); }

// Glue code here
FORCE_INLINE void write(const char *str) { while (*str) write(*str++); }
Expand Down
27 changes: 23 additions & 4 deletions Marlin/src/core/serial_hook.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ struct BaseSerial : public SerialBase< BaseSerial<SerialT> >, public SerialT {
bool connected() { return CALL_IF_EXISTS(bool, static_cast<SerialT*>(this), connected);; }
void flushTX() { CALL_IF_EXISTS(void, static_cast<SerialT*>(this), flushTX); }

SerialFeature features(serial_index_t index) const { return CALL_IF_EXISTS(SerialFeature, static_cast<const SerialT*>(this), features, index); }

// We have 2 implementation of the same method in both base class, let's say which one we want
using SerialT::available;
using SerialT::read;
Expand Down Expand Up @@ -98,10 +100,11 @@ struct ConditionalSerial : public SerialBase< ConditionalSerial<SerialT> > {
bool connected() { return CALL_IF_EXISTS(bool, &out, connected); }
void flushTX() { CALL_IF_EXISTS(void, &out, flushTX); }

int available(serial_index_t ) { return (int)out.available(); }
int read(serial_index_t ) { return (int)out.read(); }
int available(serial_index_t) { return (int)out.available(); }
int read(serial_index_t) { return (int)out.read(); }
int available() { return (int)out.available(); }
int read() { return (int)out.read(); }
SerialFeature features(serial_index_t index) const { return CALL_IF_EXISTS(SerialFeature, &out, features, index); }

ConditionalSerial(bool & conditionVariable, SerialT & out, const bool e) : BaseClassT(e), condition(conditionVariable), out(out) {}
};
Expand All @@ -126,6 +129,7 @@ struct ForwardSerial : public SerialBase< ForwardSerial<SerialT> > {
int read(serial_index_t) { return (int)out.read(); }
int available() { return (int)out.available(); }
int read() { return (int)out.read(); }
SerialFeature features(serial_index_t index) const { return CALL_IF_EXISTS(SerialFeature, &out, features, index); }

ForwardSerial(const bool e, SerialT & out) : BaseClassT(e), out(out) {}
};
Expand Down Expand Up @@ -163,9 +167,15 @@ struct RuntimeSerial : public SerialBase< RuntimeSerial<SerialT> >, public Seria

// Underlying implementation might use Arduino's bool operator
bool connected() {
return Private::HasMember_connected<SerialT>::value ? CALL_IF_EXISTS(bool, static_cast<SerialT*>(this), connected) : static_cast<SerialT*>(this)->operator bool();
return Private::HasMember_connected<SerialT>::value
? CALL_IF_EXISTS(bool, static_cast<SerialT*>(this), connected)
: static_cast<SerialT*>(this)->operator bool();
}
void flushTX() { CALL_IF_EXISTS(void, static_cast<SerialT*>(this), flushTX); }

void flushTX() { CALL_IF_EXISTS(void, static_cast<SerialT*>(this), flushTX); }

// Append Hookable for this class
SerialFeature features(serial_index_t index) const { return SerialFeature::Hookable | CALL_IF_EXISTS(SerialFeature, static_cast<const SerialT*>(this), features, index); }

void setHook(WriteHook writeHook = 0, EndOfMessageHook eofHook = 0, void * userPointer = 0) {
// Order is important here as serial code can be called inside interrupts
Expand Down Expand Up @@ -251,6 +261,15 @@ struct MultiSerial : public SerialBase< MultiSerial<Serial0T, Serial1T, offset,
if (portMask.enabled(SecondOutput)) CALL_IF_EXISTS(void, &serial1, flushTX);
}

// Forward feature queries
SerialFeature features(serial_index_t index) const {
if (index.within(0 + offset, step + offset - 1))
return serial0.features(index);
else if (index.within(step + offset, 2 * step + offset - 1))
return serial1.features(index);
return SerialFeature::None;
}

MultiSerial(Serial0T & serial0, Serial1T & serial1, const SerialMask mask = Both, const bool e = false) :
BaseClassT(e),
portMask(mask), serial0(serial0), serial1(serial1) {}
Expand Down
2 changes: 2 additions & 0 deletions Marlin/src/feature/meatpack.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ struct MeatpackSerial : public SerialBase <MeatpackSerial < SerialT >> {
// Existing instances implement Arduino's operator bool, so use that if it's available
bool connected() { return Private::HasMember_connected<SerialT>::value ? CALL_IF_EXISTS(bool, &out, connected) : (bool)out; }
void flushTX() { CALL_IF_EXISTS(void, &out, flushTX); }
SerialFeature features(serial_index_t index) const { return SerialFeature::MeatPack | CALL_IF_EXISTS(SerialFeature, &out, features, index); }


int available(serial_index_t index) {
if (charCount) return charCount; // The buffer still has data
Expand Down
5 changes: 5 additions & 0 deletions Marlin/src/gcode/gcode_d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@
dump_delay_accuracy_check();
break;

case 7: // D7 dump the current serial port type (hence configuration)
SERIAL_ECHOLNPAIR("Current serial configuration RX_BS:", RX_BUFFER_SIZE, ", TX_BS:", TX_BUFFER_SIZE);
SERIAL_ECHOLN(gtn(&SERIAL_IMPL));
break;

case 100: { // D100 Disable heaters and attempt a hard hang (Watchdog Test)
SERIAL_ECHOLNPGM("Disabling heaters and attempting to trigger Watchdog");
SERIAL_ECHOLNPGM("(USE_WATCHDOG " TERN(USE_WATCHDOG, "ENABLED", "DISABLED") ")");
Expand Down
9 changes: 7 additions & 2 deletions Marlin/src/gcode/host/M115.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

#include "../gcode.h"
#include "../../inc/MarlinConfig.h"
#include "../queue.h" // for getting the command port


#if ENABLED(M115_GEOMETRY_REPORT)
#include "../../module/motion.h"
Expand Down Expand Up @@ -59,6 +61,9 @@ void GcodeSuite::M115() {

#if ENABLED(EXTENDED_CAPABILITIES_REPORT)

// The port that sent M115
serial_index_t port = queue.ring_buffer.command_port();

// PAREN_COMMENTS
TERN_(PAREN_COMMENTS, cap_line(PSTR("PAREN_COMMENTS"), true));

Expand All @@ -69,7 +74,7 @@ void GcodeSuite::M115() {
cap_line(PSTR("SERIAL_XON_XOFF"), ENABLED(SERIAL_XON_XOFF));

// BINARY_FILE_TRANSFER (M28 B1)
cap_line(PSTR("BINARY_FILE_TRANSFER"), ENABLED(BINARY_FILE_TRANSFER));
cap_line(PSTR("BINARY_FILE_TRANSFER"), ENABLED(BINARY_FILE_TRANSFER)); // TODO: Use SERIAL_IMPL.has_feature(port, SerialFeature::BinaryFileTransfer) once implemented

// EEPROM (M500, M501)
cap_line(PSTR("EEPROM"), ENABLED(EEPROM_SETTINGS));
Expand Down Expand Up @@ -148,7 +153,7 @@ void GcodeSuite::M115() {
cap_line(PSTR("COOLER_TEMPERATURE"), ENABLED(HAS_COOLER));

// MEATPACK Compression
cap_line(PSTR("MEATPACK"), ENABLED(HAS_MEATPACK));
cap_line(PSTR("MEATPACK"), SERIAL_IMPL.has_feature(port, SerialFeature::MeatPack));

// Machine Geometry
#if ENABLED(M115_GEOMETRY_REPORT)
Expand Down