iluz/Arduino/libraries/ICMPPing/ICMPPing.cpp

382 lines
10 KiB
C++

/*
* Copyright (c) 2010 by Blake Foster <blfoster@vassar.edu>
*
* This file is free software; you can redistribute it and/or modify
* it under the terms of either the GNU General Public License version 2
* or the GNU Lesser General Public License version 2.1, both as
* published by the Free Software Foundation.
*/
#include "ICMPPing.h"
#include <util.h>
#ifdef ICMPPING_INSERT_YIELDS
#define ICMPPING_DOYIELD() delay(2)
#else
#define ICMPPING_DOYIELD()
#endif
inline uint16_t _makeUint16(const uint8_t& highOrder, const uint8_t& lowOrder)
{
// make a 16-bit unsigned integer given the low order and high order bytes.
// lowOrder first because the Arduino is little endian.
uint8_t value [] = {lowOrder, highOrder};
return *(uint16_t *)&value;
}
uint16_t _checksum(const ICMPEcho& echo)
{
// calculate the checksum of an ICMPEcho with all fields but icmpHeader.checksum populated
unsigned long sum = 0;
// add the header, bytes reversed since we're using little-endian arithmetic.
sum += _makeUint16(echo.icmpHeader.type, echo.icmpHeader.code);
// add id and sequence
sum += echo.id + echo.seq;
// add time, one half at a time.
uint16_t const * time = (uint16_t const *)&echo.time;
sum += *time + *(time + 1);
// add the payload
for (uint8_t const * b = echo.payload; b < echo.payload + sizeof(echo.payload); b+=2)
{
sum += _makeUint16(*b, *(b + 1));
}
// ones complement of ones complement sum
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return ~sum;
}
ICMPEcho::ICMPEcho(uint8_t type, uint16_t _id, uint16_t _seq, uint8_t * _payload)
: seq(_seq), id(_id), time(millis())
{
memcpy(payload, _payload, REQ_DATASIZE);
icmpHeader.type = type;
icmpHeader.code = 0;
icmpHeader.checksum = _checksum(*this);
}
ICMPEcho::ICMPEcho()
: seq(0), id(0), time(0)
{
memset(payload, 0, sizeof(payload));
icmpHeader.code = 0;
icmpHeader.type = 0;
icmpHeader.checksum = 0;
}
void ICMPEcho::serialize(uint8_t * binData) const
{
*(binData++) = icmpHeader.type;
*(binData++) = icmpHeader.code;
*(uint16_t *)binData = htons(icmpHeader.checksum); binData += 2;
*(uint16_t *)binData = htons(id); binData += 2;
*(uint16_t *)binData = htons(seq); binData += 2;
*(icmp_time_t *) binData = htonl(time); binData += 4;
memcpy(binData, payload, sizeof(payload));
}
void ICMPEcho::deserialize(uint8_t const * binData)
{
icmpHeader.type = *(binData++);
icmpHeader.code = *(binData++);
icmpHeader.checksum = ntohs(*(uint16_t *)binData); binData += 2;
id = ntohs(*(uint16_t *)binData); binData += 2;
seq = ntohs(*(uint16_t *)binData); binData += 2;
if (icmpHeader.type != TIME_EXCEEDED)
{
time = ntohl(*(icmp_time_t *)binData); binData += 4;
}
memcpy(payload, binData, sizeof(payload));
}
uint16_t ICMPPing::ping_timeout = PING_TIMEOUT;
ICMPPing::ICMPPing(SOCKET socket, uint8_t id) :
#ifdef ICMPPING_ASYNCH_ENABLE
_curSeq(0), _numRetries(0), _asyncstart(0), _asyncstatus(BAD_RESPONSE),
#endif
_id(id), _nextSeq(0), _socket(socket), _attempt(0)
{
memset(_payload, 0x1A, REQ_DATASIZE);
}
void ICMPPing::setPayload(uint8_t * payload)
{
memcpy(_payload, payload, REQ_DATASIZE);
}
void ICMPPing::openSocket()
{
W5100.execCmdSn(_socket, Sock_CLOSE);
W5100.writeSnIR(_socket, 0xFF);
W5100.writeSnMR(_socket, SnMR::IPRAW);
W5100.writeSnPROTO(_socket, IPPROTO::ICMP);
W5100.writeSnPORT(_socket, 0);
W5100.execCmdSn(_socket, Sock_OPEN);
}
void ICMPPing::operator()(const IPAddress& addr, int nRetries, ICMPEchoReply& result)
{
openSocket();
ICMPEcho echoReq(ICMP_ECHOREQ, _id, _nextSeq++, _payload);
for (_attempt=0; _attempt<nRetries; ++_attempt)
{
ICMPPING_DOYIELD();
result.status = sendEchoRequest(addr, echoReq);
if (result.status == SUCCESS)
{
byte replyAddr [4];
ICMPPING_DOYIELD();
receiveEchoReply(echoReq, addr, result);
}
if (result.status == SUCCESS)
{
break;
}
}
W5100.execCmdSn(_socket, Sock_CLOSE);
W5100.writeSnIR(_socket, 0xFF);
}
ICMPEchoReply ICMPPing::operator()(const IPAddress& addr, int nRetries)
{
ICMPEchoReply reply;
operator()(addr, nRetries, reply);
return reply;
}
Status ICMPPing::sendEchoRequest(const IPAddress& addr, const ICMPEcho& echoReq)
{
// I wish there were a better way of doing this, but if we use the uint32_t
// cast operator, we're forced to (1) cast away the constness, and (2) deal
// with an endianness nightmare.
uint8_t addri [] = {addr[0], addr[1], addr[2], addr[3]};
W5100.writeSnDIPR(_socket, addri);
W5100.writeSnTTL(_socket, 128);
// The port isn't used, becuause ICMP is a network-layer protocol. So we
// write zero. This probably isn't actually necessary.
W5100.writeSnDPORT(_socket, 0);
uint8_t serialized [sizeof(ICMPEcho)];
echoReq.serialize(serialized);
W5100.send_data_processing(_socket, serialized, sizeof(ICMPEcho));
W5100.execCmdSn(_socket, Sock_SEND);
while ((W5100.readSnIR(_socket) & SnIR::SEND_OK) != SnIR::SEND_OK)
{
if (W5100.readSnIR(_socket) & SnIR::TIMEOUT)
{
W5100.writeSnIR(_socket, (SnIR::SEND_OK | SnIR::TIMEOUT));
return SEND_TIMEOUT;
}
ICMPPING_DOYIELD();
}
W5100.writeSnIR(_socket, SnIR::SEND_OK);
return SUCCESS;
}
void ICMPPing::receiveEchoReply(const ICMPEcho& echoReq, const IPAddress& addr, ICMPEchoReply& echoReply)
{
icmp_time_t start = millis();
while (millis() - start < ping_timeout)
{
if (W5100.getRXReceivedSize(_socket) < 1)
{
// take a break, maybe let platform do
// some background work (like on ESP8266)
ICMPPING_DOYIELD();
continue;
}
// ah! we did receive something... check it out.
uint8_t ipHeader[6];
uint8_t buffer = W5100.readSnRX_RD(_socket);
W5100.read_data(_socket, (uint16_t) buffer, ipHeader, sizeof(ipHeader));
buffer += sizeof(ipHeader);
for (int i = 0; i < 4; ++i)
echoReply.addr[i] = ipHeader[i];
uint8_t dataLen = ipHeader[4];
dataLen = (dataLen << 8) + ipHeader[5];
uint8_t serialized[sizeof(ICMPEcho)];
if (dataLen > sizeof(ICMPEcho))
dataLen = sizeof(ICMPEcho);
W5100.read_data(_socket, (uint16_t) buffer, serialized, dataLen);
echoReply.data.deserialize(serialized);
buffer += dataLen;
W5100.writeSnRX_RD(_socket, buffer);
W5100.execCmdSn(_socket, Sock_RECV);
echoReply.ttl = W5100.readSnTTL(_socket);
// Since there aren't any ports in ICMP, we need to manually inspect the response
// to see if it originated from the request we sent out.
switch (echoReply.data.icmpHeader.type) {
case ICMP_ECHOREP: {
if (echoReply.data.id == echoReq.id
&& echoReply.data.seq == echoReq.seq) {
echoReply.status = SUCCESS;
return;
}
break;
}
case TIME_EXCEEDED: {
uint8_t * sourceIpHeader = echoReply.data.payload;
unsigned int ipHeaderSize = (sourceIpHeader[0] & 0x0F) * 4u;
uint8_t * sourceIcmpHeader = echoReply.data.payload + ipHeaderSize;
// The destination ip address in the originating packet's IP header.
IPAddress sourceDestAddress(sourceIpHeader + ipHeaderSize - 4);
if (!(sourceDestAddress == addr))
continue;
uint16_t sourceId = ntohs(*(uint16_t * )(sourceIcmpHeader + 4));
uint16_t sourceSeq = ntohs(*(uint16_t * )(sourceIcmpHeader + 6));
if (sourceId == echoReq.id && sourceSeq == echoReq.seq) {
echoReply.status = BAD_RESPONSE;
return;
}
break;
}
}
}
echoReply.status = NO_RESPONSE;
}
#ifdef ICMPPING_ASYNCH_ENABLE
/*
* When ICMPPING_ASYNCH_ENABLE is defined, we have access to the
* asyncStart()/asyncComplete() methods from the API.
*/
bool ICMPPing::asyncSend(ICMPEchoReply& result)
{
ICMPEcho echoReq(ICMP_ECHOREQ, _id, _curSeq, _payload);
Status sendOpResult(NO_RESPONSE);
bool sendSuccess = false;
for (uint8_t i=_attempt; i<_numRetries; ++i)
{
_attempt++;
ICMPPING_DOYIELD();
sendOpResult = sendEchoRequest(_addr, echoReq);
if (sendOpResult == SUCCESS)
{
sendSuccess = true; // it worked
sendOpResult = ASYNC_SENT; // we're doing this async-style, force the status
_asyncstart = millis(); // not the start time, for timeouts
break; // break out of this loop, 'cause we're done.
}
}
_asyncstatus = sendOpResult; // keep track of this, in case the ICMPEchoReply isn't re-used
result.status = _asyncstatus; // set the result, in case the ICMPEchoReply is checked
return sendSuccess; // return success of send op
}
bool ICMPPing::asyncStart(const IPAddress& addr, int nRetries, ICMPEchoReply& result)
{
openSocket();
// stash our state, so we can access
// in asynchSend()/asyncComplete()
_numRetries = nRetries;
_attempt = 0;
_curSeq = _nextSeq++;
_addr = addr;
return asyncSend(result);
}
bool ICMPPing::asyncComplete(ICMPEchoReply& result)
{
if (_asyncstatus != ASYNC_SENT)
{
// we either:
// - didn't start an async request;
// - failed to send; or
// - are no longer waiting on this packet.
// either way, we're done
return true;
}
if (W5100.getRXReceivedSize(_socket))
{
// ooooh, we've got a pending reply
ICMPEcho echoReq(ICMP_ECHOREQ, _id, _curSeq, _payload);
receiveEchoReply(echoReq, _addr, result);
_asyncstatus = result.status; // make note of this status, whatever it is.
return true; // whatever the result of the receiveEchoReply(), the async op is done.
}
// nothing yet... check if we've timed out
if ( (millis() - _asyncstart) > ping_timeout)
{
// yep, we've timed out...
if (_attempt < _numRetries)
{
// still, this wasn't our last attempt, let's try again
if (asyncSend(result))
{
// another send has succeeded
// we'll wait for that now...
return false;
}
// this send has failed. too bad,
// we are done.
return true;
}
// we timed out and have no more attempts left...
// hello? is anybody out there?
// guess not:
result.status = NO_RESPONSE;
return true;
}
// have yet to time out, will wait some more:
return false; // results still not in
}
#endif /* ICMPPING_ASYNCH_ENABLE */