2

I am trying to use an sprintf with an IPAddress and I am not sure why Serial.print() is able to convert IPAddress to a reasonable human readable representation. I mean, presumably it uses an overloaded function that detects what type the parameter is. I want to somehow leverage that functionality ina printf or sprintf.

What I know:

IPAddress ip = WiFi.localIP();
Serial.println(ip);

works great and outputs "192.168.4.1".

char *buf = malloc(128);
sprintf(buf,"%s",String(ip).c_str());

generates buf as "17082560" which isn't wrong, but its not the usual format for an IP address.

I could write a function to output it the way I want, but I figured there must be some relatively simple way to utilize the functionality that Serial.print() provides but somehow channel it to an sprintf() call. Or any other simple way to provide a formatted IP output would be appreciated.

3 Answers 3

2

Whereas KIIV's answer solves your problem, it does not address some very interesting parts of your original question:

I am not sure why Serial.print() is able to convert IPAddress to a reasonable human readable representation

This works thanks to a couple of abstract classes in the Arduino library called Print and Printable. A Print object is anything that can output text, like a serial port, a network connection or an LCD. A Printable object is anything that can be converted to a text representation and sent through a Print object.

From Print.h:

class Print
{
    // ...
    virtual size_t write(uint8_t) = 0;
    // ...
    size_t print(const String &);
    size_t print(const char[]);
    size_t print(unsigned int, int = DEC);
    size_t print(const Printable&);
    // ...
    size_t println(const String &s);
    // ...
};

In other words, in order to create a Print, you only have to implement write(uint8_t), which should output a single character. Then you get all the print() and println() methods.

From Printable.h:

class Printable
{
  public:
    virtual size_t printTo(Print& p) const = 0;
};

This means you can make any object Printable by implementing printTo(Print&).

From IPAddress.h:

class IPAddress : public Printable {
    // ...
};

This means IPAddress objects are Printable: they know how to print themselves into any Print. You can find the implementation of IPAddress::printTo(Print&) at the end of IPAddress.cpp.

Thus, to answer your question, Serial.print() does not really know how to create a textual representation of the IPAddress, but IPAddress knows how to send itself through Serial, or any other Print object.

I want to somehow leverage that functionality [...] there must be some relatively simple way to utilize the functionality that Serial.print() provides but somehow channel it to an sprintf() call.

The functionality is not provided by Serial.print(): it's the fact that the IPAddress is Printable. In order to leverage that, you have to create a Print object. You cannot use sprintf() for that, but you can create a Print object which behaves like sprintf() and writes into a C string:

// Write into a C string (a char array), a la sprintf().
// Warning: there is no check for buffer overflow.
class StringPrinter : public Print
{
public:
    StringPrinter(char *buffer) : buf(buffer), pos(0) {}
    virtual size_t write(uint8_t c)
    {
        buf[pos++] = c;  // add the character to the string
        buf[pos] = 0;    // null perminator
        return 1;        // one character written
    }
private:
    char *buf;
    size_t pos;
};

which could be used like this:

char buf[16];
StringPrinter(buf).print(ip);

For the sake of simplicity, I would generally prefer the straight sprintf() solution. However, if you are short on flash, the code above is likely to be smaller, as sprintf() is large and you are very likely to be already using Print in your program.

1
  • Awesome information about the abstract classes. This is kind of what I was dying to know.
    – Octopus
    Oct 27, 2016 at 22:38
3

In short: hex(17082560) = 0x0104A8C0 where: 0x01 = 1, 0x04 = 4, 0xA8 = 168 and 0xC0 = 192. See the pattern? It's printed as a single uint32_t.

Of course endianness is reversed as platform is little endian but IP Adresses are in network byte order (big endian)

And because you didn't understand this answer, there is also whole code:

uint32_t ip = (uint32_t) WiFi.localIP();
sprintf(buf, "%u.%u.%u.%u", ip & 0xFF, (ip>>8) & 0xFF, (ip>>16) & 0xFF, (ip>>24) & 0xFF);

Maybe even better:

IPAddress ip = WiFi.localIP();
sprintf(buf, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
3
  • yes, as I said, "which isn't wrong, but its not the usual format for an IP address". this doesn't answer my question. Thanks, though.
    – Octopus
    Oct 25, 2016 at 20:16
  • @Octopus: I hoped you'll figure this out by yourself. Anyway code added (untested)
    – KIIV
    Oct 25, 2016 at 20:31
  • right on. now i'm having one of those "why didn't i think of that" moments. perfect!
    – Octopus
    Oct 25, 2016 at 22:01
2

There is also a one-line solution just as you wish. Instead of String(ip) use ip.toString().

IPAddress ip = WiFi.localIP();
char *buf = malloc(128);
sprintf(buf, "%s", ip.toString().c_str());

Background: The function Serial.println(ip); calls Print::print(const Printable& x), which in turn calls IPAddress::printTo(Print& p).

size_t Print::print(const Printable& x) {
    return x.printTo(*this);
}

Alternatively, String IPAddress::toString() creates the desired string and reserves just the needed amount of memory (code from ESP8266 2.7.4)

String IPAddress::toString() const
{
    StreamString sstr;
#if LWIP_IPV6
    if (isV6())
        sstr.reserve(40); // 8 shorts x 4 chars each + 7 colons + nullterm
    else
#endif
        sstr.reserve(16); // 4 bytes with 3 chars max + 3 dots + nullterm, or '(IP unset)'
    printTo(sstr);
    return sstr;
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.