Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e569a74
add defines for needed PROGMEM functions
Jul 11, 2025
1a6d44b
implemented writeString(const __FlashStringHelper* ...), writeString_P()
Jul 11, 2025
d2fa270
Merge branch 'master' into implement-fstring-topics
Jul 11, 2025
07efb35
Merge branch 'master' into implement-fstring-topics
hmueller01 Jul 11, 2025
64709f1
fix merge failure
hmueller01 Jul 13, 2025
b2fb86c
added writeString(const __FlashStringHelper* ...), writeString_P()
hmueller01 Jul 13, 2025
c461807
added __FlashStringHelper
hmueller01 Jul 13, 2025
ff1997e
Merge branch 'master' into implement-fstring-topics
hmueller01 Jul 13, 2025
5019584
Merge branch 'master' into implement-fstring-topics
hmueller01 Jul 13, 2025
79bd37d
Merge branch 'master' into implement-fstring-topics
hmueller01 Jul 19, 2025
f8374e8
Merge branch 'master' into implement-fstring-topics
hmueller01 Oct 20, 2025
07ebf85
do not use prog_uint8_t, deprecated
hmueller01 Oct 20, 2025
42d278f
Merge branch 'master' into implement-fstring-topics
hmueller01 Oct 21, 2025
92ce07f
Merge branch 'master' into implement-fstring-topics
hmueller01 Oct 23, 2025
3de4409
removed writeString(__FlashStringHelper*), added beginPublishImpl(), …
hmueller01 Oct 23, 2025
2df1cf2
added F() macro
hmueller01 Oct 23, 2025
30d9095
Merge branch 'master' into implement-fstring-topics
hmueller01 Oct 23, 2025
1aec205
implemented writeStringImpl() using template, added __FlashStringHelp…
hmueller01 Oct 24, 2025
3ba5637
fixed wrong return type of writeStringImpl()
hmueller01 Oct 24, 2025
1fdd6dc
shorten source code size for getting string length
hmueller01 Oct 24, 2025
541876a
removed template implementation by classic parameter
hmueller01 Oct 29, 2025
ee3b134
Merge branch 'master' into implement-fstring-topics
hmueller01 Oct 29, 2025
2d7b1b2
added publish(__FlashStringHelper* topic, __FlashStringHelper* payloa…
hmueller01 Oct 29, 2025
2fe356c
make all only beginPublishImpl / writeStringImpl calling functions in…
hmueller01 Oct 30, 2025
02f55f9
fix compiler error: moved public inline functions to header file
hmueller01 Nov 2, 2025
ae7cd3e
fix compiler error: moved public inline functions to header file
hmueller01 Nov 2, 2025
5ea9b74
implement write_P(PGM_P string)
hmueller01 Nov 2, 2025
443097d
Initial revision
hmueller01 Nov 2, 2025
1a3b765
fix arduino-cli compile error
hmueller01 Nov 2, 2025
b7fe4c3
implemented bool subscribeImpl(bool progmem, ...) and bool unsubscrib…
hmueller01 Nov 5, 2025
f455953
added subscribe examples
hmueller01 Nov 6, 2025
4168491
Merge branch 'master' into implement-fstring-topics
hmueller01 Nov 6, 2025
54de124
Merge branch 'master' into implement-fstring-topics
hmueller01 Nov 9, 2025
f79d377
Merge branch 'master' into implement-fstring-topics
hmueller01 Nov 9, 2025
1676861
tag unused parameter
hmueller01 Nov 9, 2025
d1d75ed
Merge branch 'master' into implement-fstring-topics
hmueller01 Nov 10, 2025
94a83df
use C++ casting
hmueller01 Nov 10, 2025
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
123 changes: 102 additions & 21 deletions src/PubSubClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,10 @@ bool PubSubClient::publish(const char* topic, const char* payload, bool retained
return publish(topic, payload, MQTT_QOS0, retained);
}

bool PubSubClient::publish(const __FlashStringHelper* topic, const char* payload, uint8_t qos, bool retained) {
return publish(topic, (const uint8_t*)payload, payload ? strnlen(payload, MQTT_MAX_POSSIBLE_PACKET_SIZE) : 0, qos, retained);
}

bool PubSubClient::publish(const char* topic, const char* payload, uint8_t qos, bool retained) {
return publish(topic, (const uint8_t*)payload, payload ? strnlen(payload, MQTT_MAX_POSSIBLE_PACKET_SIZE) : 0, qos, retained);
}
Expand All @@ -545,11 +549,23 @@ bool PubSubClient::publish(const char* topic, const uint8_t* payload, size_t ple
return false;
}

bool PubSubClient::publish_P(const char* topic, const char* payload, bool retained) {
bool PubSubClient::publish(const __FlashStringHelper* topic, const uint8_t* payload, size_t plength, uint8_t qos, bool retained) {
if (beginPublish(topic, plength, qos, retained)) {
size_t rc = write(payload, plength);
return endPublish() && (rc == plength);
}
return false;
}

bool PubSubClient::publish_P(const char* topic, PGM_P payload, bool retained) {
return publish_P(topic, payload, MQTT_QOS0, retained);
}

bool PubSubClient::publish_P(const char* topic, const char* payload, uint8_t qos, bool retained) {
bool PubSubClient::publish_P(const __FlashStringHelper* topic, PGM_P payload, uint8_t qos, bool retained) {
return publish_P(topic, (const uint8_t*)payload, payload ? strnlen_P(payload, MQTT_MAX_POSSIBLE_PACKET_SIZE) : 0, qos, retained);
}

bool PubSubClient::publish_P(const char* topic, PGM_P payload, uint8_t qos, bool retained) {
return publish_P(topic, (const uint8_t*)payload, payload ? strnlen_P(payload, MQTT_MAX_POSSIBLE_PACKET_SIZE) : 0, qos, retained);
}

Expand All @@ -565,27 +581,50 @@ bool PubSubClient::publish_P(const char* topic, const uint8_t* payload, size_t p
return false;
}

bool PubSubClient::publish_P(const __FlashStringHelper* topic, const uint8_t* payload, size_t plength, uint8_t qos, bool retained) {
if (beginPublish(topic, plength, qos, retained)) {
size_t rc = write_P(payload, plength);
return endPublish() && (rc == plength);
}
return false;
}

bool PubSubClient::beginPublish(const char* topic, size_t plength, bool retained) {
return beginPublish(topic, plength, MQTT_QOS0, retained);
}

bool PubSubClient::beginPublish(const char* topic, size_t plength, uint8_t qos, bool retained) {
template <bool PROGMEM_TOPIC, typename TopicT>
bool PubSubClient::beginPublishImpl(TopicT topic, size_t plength, uint8_t qos, bool retained) {
if (!topic) return false;
if (strlen(topic) == 0) return false; // empty topic is not allowed
if (qos > MQTT_QOS2) { // only valid QoS supported

// get topic length depending on storage (RAM vs PROGMEM)
size_t topicLen;
if (PROGMEM_TOPIC) {
topicLen = strlen_P(topic);
} else {
topicLen = strlen(topic);
}
if (topicLen == 0) return false; // empty topic is not allowed

if (qos > MQTT_QOS2) { // only valid QoS supported
ERROR_PSC_PRINTF_P("beginPublish() called with invalid QoS %u\n", qos);
return false;
}

const size_t nextMsgLen = (qos > MQTT_QOS0) ? 2 : 0; // add 2 bytes for nextMsgId if QoS > 0
// check if the header, the topic (including 2 length bytes) and nextMsgId fit into the _buffer
if (connected() && (MQTT_MAX_HEADER_SIZE + strlen(topic) + 2 + nextMsgLen <= _bufferSize)) {
if (connected() && (MQTT_MAX_HEADER_SIZE + topicLen + 2 + nextMsgLen <= _bufferSize)) {
// first write the topic at the end of the maximal variable header (MQTT_MAX_HEADER_SIZE) to the _buffer
size_t topicLen = writeString(topic, MQTT_MAX_HEADER_SIZE) - MQTT_MAX_HEADER_SIZE;
if (PROGMEM_TOPIC) {
topicLen = writeString_P(topic, MQTT_MAX_HEADER_SIZE) - MQTT_MAX_HEADER_SIZE;
} else {
topicLen = writeString(topic, MQTT_MAX_HEADER_SIZE) - MQTT_MAX_HEADER_SIZE;
}
if (qos > MQTT_QOS0) {
// if QoS 1 or 2, we need to send the nextMsgId (packet identifier) after topic
writeNextMsgId(MQTT_MAX_HEADER_SIZE + topicLen);
}
// we now know the length of the topic string (lenght + 2 bytes signalling the length) and can build the variable header information
// we now know the length of the topic string (length + 2 bytes signalling the length) and can build the variable header information
const uint8_t header = MQTTPUBLISH | MQTT_QOS_GET_HDR(qos) | (retained ? MQTTRETAINED : 0);
uint8_t hdrLen = buildHeader(header, topicLen + nextMsgLen + plength);
if (hdrLen == 0) return false; // exit here in case of header generation failure
Expand All @@ -597,6 +636,19 @@ bool PubSubClient::beginPublish(const char* topic, size_t plength, uint8_t qos,
return false;
}

bool PubSubClient::beginPublish(const char* topic, size_t plength, uint8_t qos, bool retained) {
return beginPublishImpl<false, const char*>(topic, plength, qos, retained);
}

bool PubSubClient::beginPublish(const __FlashStringHelper* topic, size_t plength, uint8_t qos, bool retained) {
// convert FlashStringHelper in PROGMEM-pointer
return beginPublishImpl<true, PGM_P>(reinterpret_cast<const char*>(topic), plength, qos, retained);
}

bool PubSubClient::beginPublish_P(PGM_P topic, size_t plength, uint8_t qos, bool retained) {
return beginPublishImpl<true, PGM_P>(topic, plength, qos, retained);
}

bool PubSubClient::endPublish() {
if (connected()) {
if (_bufferWritePos > 0) {
Expand Down Expand Up @@ -632,7 +684,7 @@ uint8_t PubSubClient::buildHeader(uint8_t header, size_t length) {
} while ((len > 0) && (hdrLen < MQTT_MAX_HEADER_SIZE - 1));

if (len > 0) {
ERROR_PSC_PRINTF_P("buildHeader() length too big %zu, left %zu\n", length, len);
ERROR_PSC_PRINTF_P("buildHeader: header=0x%02X, length too big %zu, left %zu\n", header, length, len);
return 0;
}

Expand Down Expand Up @@ -665,7 +717,7 @@ size_t PubSubClient::write_P(const uint8_t* buf, size_t size) {
*
* @param header Header byte, e.g. MQTTCONNECT, MQTTPUBLISH, MQTTSUBSCRIBE, MQTTUNSUBSCRIBE.
* @param length Length of _buffer to write.
* @return True if successfully sent, otherwise false if buildHeader() failed or buffer could not be written.
* @return True if successfully sent, otherwise false if build header failed or buffer could not be written.
*/
bool PubSubClient::writeControlPacket(uint8_t header, size_t length) {
uint8_t hdrLen = buildHeader(header, length);
Expand Down Expand Up @@ -712,6 +764,31 @@ size_t PubSubClient::writeBuffer(size_t pos, size_t size) {
return rc;
}

template <bool PROGMEM_STRING, typename StringT>
size_t PubSubClient::writeStringImpl(StringT string, size_t pos) {
if (!string) return pos;

size_t sLen;
if (PROGMEM_STRING) {
sLen = strlen_P(string);
} else {
sLen = strlen(string);
}
if ((pos + 2 + sLen <= _bufferSize) && (sLen <= 0xFFFF)) {
_buffer[pos++] = (uint8_t)(sLen >> 8);
_buffer[pos++] = (uint8_t)(sLen & 0xFF);
if (PROGMEM_STRING) {
memcpy_P(_buffer + pos, string, sLen);
} else {
memcpy(_buffer + pos, string, sLen);
}
pos += sLen;
} else {
ERROR_PSC_PRINTF_P("writeStringImpl(): string (%zu) does not fit into buf (%zu)\n", pos + 2 + sLen, _bufferSize);
}
return pos;
}

/**
* @brief Write an UTF-8 encoded string to the internal buffer at a given position. The string can have a length of 0 to 65535 bytes (depending on size of
* internal buffer). The buffer is prefixed with two bytes representing the length of the string. See section 1.5.3 of MQTT v3.1.1 protocol specification.
Expand All @@ -723,18 +800,22 @@ size_t PubSubClient::writeBuffer(size_t pos, size_t size) {
* @return New position in the internal buffer (pos + 2 + string length), or pos if a buffer overrun would occur or the string is a nullptr.
*/
size_t PubSubClient::writeString(const char* string, size_t pos) {
if (!string) return pos;
return writeStringImpl<false, const char*>(string, pos);
}

size_t sLen = strlen(string);
if ((pos + 2 + sLen <= _bufferSize) && (sLen <= 0xFFFF)) {
_buffer[pos++] = (uint8_t)(sLen >> 8);
_buffer[pos++] = (uint8_t)(sLen & 0xFF);
memcpy(_buffer + pos, string, sLen);
pos += sLen;
} else {
ERROR_PSC_PRINTF_P("writeString(): string (%zu) does not fit into buf (%zu)\n", pos + 2 + sLen, _bufferSize);
}
return pos;
/**
* @brief Write an UTF-8 encoded PROGMEM string to the internal buffer at a given position. The string can have a length of 0 to 65535 bytes (depending on
* size of internal buffer). The buffer is prefixed with two bytes representing the length of the string. See section 1.5.3 of MQTT v3.1.1 protocol
* specification.
* @note If the string does not fit in the buffer or is longer than 65535 bytes nothing is written to the buffer and the returned position is
* unchanged.
*
* @param string PROGMEM 'C' string of the data that shall be written in the buffer.
* @param pos Position in the internal buffer to write the string.
* @return New position in the internal buffer (pos + 2 + string length), or pos if a buffer overrun would occur or the string is a nullptr.
*/
size_t PubSubClient::writeString_P(PGM_P string, size_t pos) {
return writeStringImpl<true, PGM_P>(string, pos);
}

/**
Expand Down
90 changes: 88 additions & 2 deletions src/PubSubClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,15 @@ class PubSubClient : public Print {
uint8_t buildHeader(uint8_t header, size_t length);
bool writeControlPacket(uint8_t header, size_t length);
size_t writeBuffer(size_t pos, size_t size);
template <bool PROGMEM_STRING, typename StringT>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one really big disadvantage of templates...
For each type that can call this function, the compiler will generate a new blob of code.
Now it is contained as a private function, which will keep it somewhat manageble.

For publicly called functions you really should take care or else you might end up with a compiled blob for types like const char[10], const char[11], const char[...], ... well you get the point :)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, thanks for the hint. I wasn't aware that the compiler creates new code for every type. The goal was to have just one implementation. But wasting flash does not sound good. Maybe I should not use templates and instead just use a parameter:
size_t PubSubClient::writeStringImpl(bool progmem_string, const char* string, size_t pos) {
Do you think this is better?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the whole idea of templates is indeed to have the same code from a programmer's perspective. But it should be considered as 'inline' code and thus appears potentially multiple times in the binary when a different (template) type is calling it.

N.B. the same does apply to code written in .h files.

If you want to switch to using const char* then you must make sure whatever is calling this function is handling potential PGM reads.
So either wrap it into a String or feed this writeStringImpl per char.
I don't think adding a bool to indicate type is a good thing as you then do the extra implementation in that function and you potentially introduce bugs as you expect the programmer to always have this pair of bool and string to match.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't seen your answer yet ...
The point is, that I need different implementations in case of PROGMEM and normal RAM. In case of ESP8266 / ESP32 I could always use the PROGMEM functions (strlen_P() and so on). But not for AVR. There RAM and ROM needs to be handled differently. So to make this lib compatible to all kinds of MCUs I need a double implementation or a switch (which is also there in case of the template). And as this is an internal function I think I can deal with the risk. As the template implementation does have no benefit, I will change to the conventional parameter.

size_t writeStringImpl(StringT string, size_t pos);
size_t writeString(const char* string, size_t pos);
size_t writeString_P(PGM_P string, size_t pos);
size_t writeNextMsgId(size_t pos);

template <bool PROGMEM_TOPIC, typename TopicT>
bool beginPublishImpl(TopicT topic, size_t plength, uint8_t qos, bool retained);

// Add to buffer and flush if full (only to be used with beginPublish/endPublish)
size_t appendBuffer(uint8_t data);
size_t flushBuffer();
Expand Down Expand Up @@ -524,6 +530,17 @@ class PubSubClient : public Print {
*/
bool publish(const char* topic, const char* payload, uint8_t qos, bool retained);

/**
* @brief Publishes a message to the specified topic.
* @param topic The topic from __FlashStringHelper to publish to.
* @param payload The message to publish.
* @param qos The quality of service (\ref group_qos) to publish at. [0, 1, 2].
* @param retained Publish the message with the retain flag.
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool publish(const __FlashStringHelper* topic, const char* payload, uint8_t qos, bool retained);

/**
* @brief Publishes a non retained message to the specified topic using QoS 0.
* @param topic The topic to publish to.
Expand Down Expand Up @@ -557,6 +574,18 @@ class PubSubClient : public Print {
*/
bool publish(const char* topic, const uint8_t* payload, size_t plength, uint8_t qos, bool retained);

/**
* @brief Publishes a message to the specified topic.
* @param topic The topic from __FlashStringHelper to publish to.
* @param payload The message to publish.
* @param plength The length of the payload.
* @param qos The quality of service (\ref group_qos) to publish at. [0, 1, 2].
* @param retained Publish the message with the retain flag.
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool publish(const __FlashStringHelper* topic, const uint8_t* payload, size_t plength, uint8_t qos, bool retained);

/**
* @brief Publishes a message stored in PROGMEM to the specified topic using QoS 0.
* @param topic The topic to publish to.
Expand All @@ -565,7 +594,7 @@ class PubSubClient : public Print {
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool publish_P(const char* topic, const char* payload, bool retained);
bool publish_P(const char* topic, PGM_P payload, bool retained);

/**
* @brief Publishes a message stored in PROGMEM to the specified topic.
Expand All @@ -576,7 +605,18 @@ class PubSubClient : public Print {
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool publish_P(const char* topic, const char* payload, uint8_t qos, bool retained);
bool publish_P(const char* topic, PGM_P payload, uint8_t qos, bool retained);

/**
* @brief Publishes a message stored in PROGMEM to the specified topic.
* @param topic The topic from __FlashStringHelper to publish to.
* @param payload The message to publish.
* @param qos The quality of service (\ref group_qos) to publish at. [0, 1, 2].
* @param retained Publish the message with the retain flag.
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool publish_P(const __FlashStringHelper* topic, PGM_P payload, uint8_t qos, bool retained);

/**
* @brief Publishes a message stored in PROGMEM to the specified topic using QoS 0.
Expand All @@ -601,6 +641,18 @@ class PubSubClient : public Print {
*/
bool publish_P(const char* topic, const uint8_t* payload, size_t plength, uint8_t qos, bool retained);

/**
* @brief Publishes a message stored in PROGMEM to the specified topic.
* @param topic The topic from __FlashStringHelper to publish to.
* @param payload The message from PROGMEM to publish.
* @param plength The length of the payload.
* @param qos The quality of service (\ref group_qos) to publish at. [0, 1, 2].
* @param retained Publish the message with the retain flag.
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool publish_P(const __FlashStringHelper* topic, const uint8_t* payload, size_t plength, uint8_t qos, bool retained);

/**
* @brief Start to publish a message using QoS 0.
* This API:
Expand Down Expand Up @@ -634,6 +686,40 @@ class PubSubClient : public Print {
*/
bool beginPublish(const char* topic, size_t plength, uint8_t qos, bool retained);

/**
* @brief Start to publish a message using a topic from __FlashStringHelper F().
* This API:
* beginPublish(...)
* one or more calls to write(...)
* endPublish()
* Allows for arbitrarily large payloads to be sent without them having to be copied into
* a new buffer and held in memory at one time.
* @param topic The topic from __FlashStringHelper to publish to.
* @param plength The length of the payload.
* @param qos The quality of service (\ref group_qos) to publish at. [0, 1, 2].
* @param retained Publish the message with the retain flag.
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool beginPublish(const __FlashStringHelper* topic, size_t plength, uint8_t qos, bool retained);

/**
* @brief Start to publish a message using a topic in PROGMEM.
* This API:
* beginPublish_P(...)
* one or more calls to write(...)
* endPublish()
* Allows for arbitrarily large payloads to be sent without them having to be copied into
* a new buffer and held in memory at one time.
* @param topic The topic in PROGMEM to publish to.
* @param plength The length of the payload.
* @param qos The quality of service (\ref group_qos) to publish at. [0, 1, 2].
* @param retained Publish the message with the retain flag.
* @return true If the publish succeeded.
* false If the publish failed, either connection lost or message too large.
*/
bool beginPublish_P(PGM_P topic, size_t plength, uint8_t qos, bool retained);

/**
* @brief Finish sending a message that was started with a call to beginPublish.
* @return true If the publish succeeded.
Expand Down
5 changes: 5 additions & 0 deletions tests/src/lib/Arduino.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ extern void loop(void);
unsigned long millis(void);
}

class __FlashStringHelper;
#define PROGMEM
#define PGM_P const char*
#define memcpy_P memcpy
#define strlen_P strlen
#define strnlen_P strnlen
#define pgm_read_byte_near(x) *(x)
#define F(x) (reinterpret_cast<const __FlashStringHelper*>(x))

#define yield(x) {}

Expand Down