I am trying to use an sprintf with an IP address and I am not sure why Serial.print()
is able to convert an IP address to a reasonable humanly readable representation.
I mean, presumably it uses an overloaded function that detects what type the parameter is. I want to somehow leverage that functionality with 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 it's 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.
4 Answers 4
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.
-
Awesome information about the abstract classes. This is kind of what I was dying to know.Octopus– Octopus10/27/2016 22:38:03Commented Oct 27, 2016 at 22:38
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]);
-
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– Octopus10/25/2016 20:16:30Commented Oct 25, 2016 at 20:16
-
@Octopus: I hoped you'll figure this out by yourself. Anyway code added (untested)KIIV– KIIV10/25/2016 20:31:21Commented 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– Octopus10/25/2016 22:01:14Commented Oct 25, 2016 at 22:01
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;
}
"%u.%u.%u.%u", ip & 0xFF
instead of, should be
... "%u.%u.%u.%u", (ip>>0) & 0xFF,...