sodaq-nbiot-socket/src/Sodaq_nbIOT.cpp

919 lines
23 KiB
C++
Raw Normal View History

2017-10-17 21:41:58 +02:00
/*
Copyright (c) 2015-2016 Sodaq. All rights reserved.
This file is part of Sodaq_nbIOT.
Sodaq_nbIOT is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, either version 3 of
the License, or(at your option) any later version.
Sodaq_nbIOT is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with Sodaq_nbIOT. If not, see
<http://www.gnu.org/licenses/>.
*/
#include "Sodaq_nbIOT.h"
#include <Sodaq_wdt.h>
#define DEBUG
#define AT_CMD_SLEEP_TIME 10
#define STR_AT "AT"
#define STR_RESPONSE_OK "OK"
#define STR_RESPONSE_ERROR "ERROR"
#define STR_RESPONSE_CME_ERROR "+CME ERROR:"
#define STR_RESPONSE_CMS_ERROR "+CMS ERROR:"
#define DEBUG_STR_ERROR "[ERROR]: "
#define NIBBLE_TO_HEX_CHAR(i) ((i <= 9) ? ('0' + i) : ('A' - 10 + i))
#define HIGH_NIBBLE(i) ((i >> 4) & 0x0F)
#define LOW_NIBBLE(i) (i & 0x0F)
#define HEX_CHAR_TO_NIBBLE(c) ((c >= 'A') ? (c - 'A' + 0x0A) : (c - '0'))
#define HEX_PAIR_TO_BYTE(h, l) ((HEX_CHAR_TO_NIBBLE(h) << 4) + HEX_CHAR_TO_NIBBLE(l))
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
#ifdef DEBUG
#define debugPrintLn(...) { if (!this->_disableDiag && this->_diagStream) this->_diagStream->println(__VA_ARGS__); }
#define debugPrint(...) { if (!this->_disableDiag && this->_diagStream) this->_diagStream->print(__VA_ARGS__); }
#warning "Debug mode is ON"
#else
#define debugPrintLn(...)
#define debugPrint(...)
#endif
#define DEFAULT_CID "1"
#define NO_IP_ADDRESS ((IP_t)0)
#define IP_FORMAT "%d.%d.%d.%d"
#define IP_TO_TUPLE(x) (uint8_t)(((x) >> 24) & 0xFF), \
(uint8_t)(((x) >> 16) & 0xFF), \
(uint8_t)(((x) >> 8) & 0xFF), \
(uint8_t)(((x) >> 0) & 0xFF)
#define TUPLE_TO_IP(o1, o2, o3, o4) ((((IP_t)o1) << 24) | (((IP_t)o2) << 16) | \
(((IP_t)o3) << 8) | (((IP_t)o4) << 0))
#define SOCKET_FAIL -1
#define SOCKET_COUNT 7
#define NOW (uint32_t)millis()
typedef struct NameValuePair {
const char *Name;
const char *Value;
} NameValuePair;
const uint8_t nConfigCount = 3;
static NameValuePair nConfig[nConfigCount] = {
{"AUTOCONNECT", "FALSE"},
{"CR_0354_0338_SCRAMBLING", "FALSE"},
{"CR_0859_SI_AVOID", "FALSE"}
};
class Sodaq_nbIotOnOff : public Sodaq_OnOffBee {
public:
Sodaq_nbIotOnOff();
void init(int onoffPin);
void on();
void off();
bool isOn();
private:
int8_t _onoffPin;
bool _onoff_status;
};
static Sodaq_nbIotOnOff sodaq_nbIotOnOff;
static inline bool is_timedout(uint32_t from, uint32_t nr_ms) __attribute__((always_inline));
static inline bool is_timedout(uint32_t from, uint32_t nr_ms) {
return (millis() - from) > nr_ms;
}
Sodaq_nbIOT::Sodaq_nbIOT() :
_lastRSSI(0),
_CSQtime(0),
_minRSSI(-113) // dBm
{
}
// Returns true if the modem replies to "AT" commands without timing out.
bool Sodaq_nbIOT::isAlive() {
// _disableDiag = true;
println(STR_AT);
// @todo check if necessary
delay(150);
return (readResponse(NULL, 450) == ResponseOK);
}
// Initializes the modem instance. Sets the modem stream and the on-off power pins.
void Sodaq_nbIOT::init(Stream &stream, int8_t onoffPin) {
debugPrintLn("[init] started.");
initBuffer(); // safe to call multiple times
setModemStream(stream);
sodaq_nbIotOnOff.init(onoffPin);
_onoff = &sodaq_nbIotOnOff;
}
bool Sodaq_nbIOT::setRadioActive(bool on) {
print("AT+CFUN=");
println(on ? "1" : "0");
return (readResponse() == ResponseOK);
}
bool Sodaq_nbIOT::setIndicationsActive(bool on) {
print("AT+NSMI=");
println(on ? "1" : "0");
if (readResponse() != ResponseOK) {
return false;
}
print("AT+NNMI=");
println(on ? "1" : "0");
return (readResponse() == ResponseOK);
}
/*!
Read the next response from the modem
Notice that we're collecting URC's here. And in the process we could
be updating:
_socketPendingBytes[] if +UUSORD: is seen
_socketClosedBit[] if +UUSOCL: is seen
*/
ResponseTypes Sodaq_nbIOT::readResponse(char *buffer, size_t size,
CallbackMethodPtr parserMethod, void *callbackParameter,
void *callbackParameter2,
size_t *outSize, uint32_t timeout) {
ResponseTypes response = ResponseNotFound;
uint32_t from = NOW;
do {
// 250ms, how many bytes at which baudrate?
int count = readLn(buffer, size, 500);
sodaq_wdt_reset();
if (count > 0) {
if (outSize) {
*outSize = count;
}
if (_disableDiag && strncmp(buffer, "OK", 2) != 0) {
_disableDiag = false;
}
debugPrint("[rdResp]: ");
debugPrintLn(buffer);
// TODO handle socket URC
//int param1, param2;
//if (sscanf(buffer, "+UUSORD: %d,%d", &param1, &param2) == 2) {
// uint16_t socket_nr = param1;
// uint16_t nr_bytes = param2;
// debugPrint("Unsolicited: Socket ");
// debugPrint(socket_nr);
// debugPrint(": ");
// debugPrint(param2);
// debugPrintLn(" bytes pending");
// if (socket_nr < ARRAY_SIZE(_socketPendingBytes)) {
// _socketPendingBytes[socket_nr] = nr_bytes;
// }
// continue;
//}
//else if (sscanf(buffer, "+UUSOCL: %d", &param1) == 1) {
// uint16_t socket_nr = param1;
// if (socket_nr < ARRAY_SIZE(_socketPendingBytes)) {
// debugPrint("Unsolicited: Socket ");
// debugPrint(socket_nr);
// debugPrint(": ");
// debugPrintLn("closed by remote");
// _socketClosedBit[socket_nr] = true;
// if (socket_nr == _openTCPsocket) {
// _openTCPsocket = -1;
// // Report this other software layers
// if (_tcpClosedHandler) {
// _tcpClosedHandler();
// }
// }
// }
// continue;
//}
if (startsWith(STR_AT, buffer)) {
continue; // skip echoed back command
}
_disableDiag = false;
if (startsWith(STR_RESPONSE_OK, buffer)) {
return ResponseOK;
}
if (startsWith(STR_RESPONSE_ERROR, buffer) ||
startsWith(STR_RESPONSE_CME_ERROR, buffer) ||
startsWith(STR_RESPONSE_CMS_ERROR, buffer)) {
return ResponseError;
}
if (parserMethod) {
ResponseTypes parserResponse = parserMethod(response, buffer, count, callbackParameter,
callbackParameter2);
if ((parserResponse != ResponseEmpty) && (parserResponse != ResponsePendingExtra)) {
return parserResponse;
} else {
// ?
// ResponseEmpty indicates that the parser was satisfied
// Continue until "OK", "ERROR", or whatever else.
}
// Prevent calling the parser again.
// This could happen if the input line is too long. It will be split
// and the next readLn will return the next part.
// The case of "ResponsePendingExtra" is an exception to this, thus waiting for more replies to be parsed.
if (parserResponse != ResponsePendingExtra) {
parserMethod = 0;
}
}
// at this point, the parserMethod has ran and there is no override response from it,
// so if there is some other response recorded, return that
// (otherwise continue iterations until timeout)
if (response != ResponseNotFound) {
debugPrintLn("** response != ResponseNotFound");
return response;
}
}
delay(10); // TODO Why do we need this delay?
} while (!is_timedout(from, timeout));
if (outSize) {
*outSize = 0;
}
debugPrintLn("[rdResp]: timed out");
return ResponseTimeout;
}
bool Sodaq_nbIOT::setApn(const char *apn) {
print("AT+CGDCONT=" DEFAULT_CID ",\"IP\",\"");
print(apn);
println("\"");
return (readResponse() == ResponseOK);
}
bool Sodaq_nbIOT::setCdp(const char *cdp) {
print("AT+NCDP=");
println(cdp);
return (readResponse() == ResponseOK);
}
// Turns on and initializes the modem, then connects to the network and activates the data connection.
bool Sodaq_nbIOT::connect(const char *apn, const char *cdp, const char *forceOperator) {
if (!on()) {
return false;
}
if (!setRadioActive(false)) {
return false;
}
if (!checkAndApplyNconfig()) {
return false;
}
reboot();
if (!on()) {
return false;
}
if (!setApn(apn) || !setCdp(cdp)) {
return false;
}
// TODO turn on
if (!setIndicationsActive(false)) {
return false;
}
if (!setRadioActive(true)) {
return false;
}
if (forceOperator && forceOperator[0] != '\0') {
print("AT+COPS=1,2,\"");
print(forceOperator);
println("\"");
if (readResponse() != ResponseOK) {
return false;
}
}
if (!waitForSignalQuality()) {
return false;
}
if (!attachGprs()) {
return false;
}
// If we got this far we succeeded
return true;
}
void Sodaq_nbIOT::reboot() {
println("AT+NRB");
// wait up to 2000ms for the modem to come up
uint32_t start = millis();
while ((readResponse() != ResponseOK) && !is_timedout(start, 2000)) {}
}
bool Sodaq_nbIOT::checkAndApplyNconfig() {
bool applyParam[nConfigCount];
println("AT+NCONFIG?");
if (readResponse<bool, uint8_t>(_nconfigParser, applyParam, NULL) == ResponseOK) {
for (uint8_t i = 0; i < nConfigCount; i++) {
debugPrint(nConfig[i].Name);
if (!applyParam[i]) {
debugPrintLn("... CHANGE");
setNconfigParam(nConfig[i].Name, nConfig[i].Value);
} else {
debugPrintLn("... OK");
}
}
return true;
}
return false;
}
bool Sodaq_nbIOT::setNconfigParam(const char *param, const char *value) {
print("AT+NCONFIG=");
print(param);
print(",");
println(value);
return readResponse() == ResponseOK;
}
ResponseTypes
Sodaq_nbIOT::_nconfigParser(ResponseTypes &response, const char *buffer, size_t size, bool *nconfigEqualsArray,
uint8_t *dummy) {
if (!nconfigEqualsArray) {
return ResponseError;
}
char name[32];
char value[32];
if (sscanf(buffer, "+NCONFIG: %[^,],%[^\r]", name, value) == 2) {
for (uint8_t i = 0; i < nConfigCount; i++) {
if (strcmp(nConfig[i].Name, name) == 0) {
if (strcmp(nConfig[i].Value, value) == 0) {
nconfigEqualsArray[i] = true;
break;
}
}
}
return ResponsePendingExtra;
}
return ResponseError;
}
bool Sodaq_nbIOT::attachGprs(uint32_t timeout) {
uint32_t start = millis();
uint32_t delay_count = 500;
while (!is_timedout(start, timeout)) {
println("AT+CGATT=1");
if (readResponse() == ResponseOK) {
return true;
}
sodaq_wdt_safe_delay(delay_count);
// Next time wait a little longer, but not longer than 5 seconds
if (delay_count < 5000) {
delay_count += 1000;
}
}
return false;
}
/**
* Turns on and initializes the modem, then connects to the network.
*
* @author edvidan
* @param apn
* @param forceOperator
* @return
*/
bool Sodaq_nbIOT::connectSocket() {
// if (!on()) {
// return false;
// }
// if (!setRadioActive(false)) {
// return false;
// }
// if (!checkAndApplyNconfig()) {
// return false;
// }
// reboot();
//
// if (!on()) {
// return false;
// }
// TODO turn on
// if (!setIndicationsActive(false)) {
// return false;
// }
// if (!setRadioActive(true)) {
// return false;
// }
// if (forceOperator && forceOperator[0] != '\0') {
// print("AT+COPS=1,2,\"");
// print(forceOperator);
// println("\"");
//
// if (readResponse() != ResponseOK) {
// return false;
// }
// }
// if (!waitForSignalQuality()) {
// return false;
// }
// if (!attachGprs()) {
// return false;
// }
// If we got this far we succeeded
return true;
}
int Sodaq_nbIOT::createSocket(uint16_t localPort) {
// only Datagram/UDP is supported
print("AT+NSOCR=DGRAM,17,");
print(localPort);
println(",1"); // enable incoming message URC (NSONMI)
delay(500);
uint8_t socket;
if (readResponse<uint8_t, uint8_t>(_createSocketParser, &socket, NULL) == ResponseOK) {
return socket;
}
return SOCKET_FAIL;
}
ResponseTypes Sodaq_nbIOT::_createSocketParser(ResponseTypes &response, const char *buffer, size_t size,
uint8_t *socket, uint8_t *dummy) {
if (!socket) {
return ResponseError;
}
int value;
if (sscanf(buffer, "%d", &value) == 1) {
*socket = value;
return ResponseEmpty;
}
return ResponseError;
}
bool Sodaq_nbIOT::sendSocket(uint8_t socket, const char *host, uint16_t port, const char *buffer, size_t size) {
print("AT+NSOST=");
print(socket);
print(",");
print(host);
print(",");
print(port);
print(",");
print(size); // why size? not encoded hex size? no idea...
print(",");
for (uint16_t i = 0; i < size; ++i) {
print(static_cast<char>(NIBBLE_TO_HEX_CHAR(HIGH_NIBBLE(buffer[i]))));
print(static_cast<char>(NIBBLE_TO_HEX_CHAR(LOW_NIBBLE(buffer[i]))));
}
println();
delay(AT_CMD_SLEEP_TIME);
uint8_t sent;
return readResponse<uint8_t, uint8_t>(_sendSocketParser, NULL, &sent) == ResponseOK && sent > 0;
}
ResponseTypes Sodaq_nbIOT::_sendSocketParser(ResponseTypes &response, const char *buffer, size_t size,
uint8_t *socket, uint8_t *sent) {
if (!socket) {
return ResponseError;
}
int value;
int value2;
if (sscanf(buffer, "%d,%d", &value, &value2) == 1) {
*sent = value2;
return ResponseEmpty;
}
return ResponseError;
}
size_t Sodaq_nbIOT::socketReceive(uint8_t socket, char *buffer, size_t size) {
print("AT+NSORF=");
print(socket);
print(",");
println(size);
if (readResponse<uint8_t, char>(_socketReceiveParser, &socket, buffer, &size) == ResponseOK) {
return size;
}
return 0;
}
ResponseTypes Sodaq_nbIOT::_socketReceiveParser(ResponseTypes &response, const char *buffer, size_t size,
uint8_t *socket, char *parsedBuffer) {
if (!socket) {
return ResponseError;
}
// <socket>,<ip_addr>,<port>,<length>,<data>,<remaining_length>
if (sscanf(buffer, "%d,%s,%d,%d,%s,%d", NULL, NULL, NULL, NULL, parsedBuffer, NULL) == 1) {
return ResponseEmpty;
}
return ResponseError;
}
size_t Sodaq_nbIOT::socketBytesPending(uint8_t socket) {
int count = readLn(_inputBuffer, _inputBufferSize, 250);
if (count == 0) {
return 0;
}
int inc_socket;
int length;
if (startsWith("+NSONMI:", _inputBuffer)) {
sscanf(_inputBuffer, "+NSONMI:%d,%d", &inc_socket, &length);
return length;
}
}
bool Sodaq_nbIOT::closeSocket(uint8_t socket) {
print("AT+NSOCL=");
println(socket);
return readResponse() == ResponseOK;
}
// Disconnects the modem from the network.
bool Sodaq_nbIOT::disconnect() {
println("AT+CGATT=0");
return (readResponse(NULL, 40000) == ResponseOK);
}
// Returns true if the modem is connected to the network and has an activated data connection.
bool Sodaq_nbIOT::isConnected() {
uint8_t value = 0;
println("AT+CGATT?");
if (readResponse<uint8_t, uint8_t>(_cgattParser, &value, NULL) == ResponseOK) {
return (value == 1);
}
return false;
}
// Gets the Received Signal Strength Indication in dBm and Bit Error Rate.
// Returns true if successful.
bool Sodaq_nbIOT::getRSSIAndBER(int8_t *rssi, uint8_t *ber) {
static char berValues[] = {49, 43, 37, 25, 19, 13, 7, 0}; // 3GPP TS 45.008 [20] subclause 8.2.4
println("AT+CSQ");
int csqRaw = 0;
int berRaw = 0;
if (readResponse<int, int>(_csqParser, &csqRaw, &berRaw) == ResponseOK) {
*rssi = ((csqRaw == 99) ? 0 : convertCSQ2RSSI(csqRaw));
*ber = ((berRaw == 99 || static_cast<size_t>(berRaw) >= sizeof(berValues)) ? 0 : berValues[berRaw]);
return true;
}
return false;
}
/*
The range is the following:
0: -113 dBm or less
1: -111 dBm
2..30: from -109 to -53 dBm with 2 dBm steps
31: -51 dBm or greater
99: not known or not detectable or currently not available
*/
int8_t Sodaq_nbIOT::convertCSQ2RSSI(uint8_t csq) const {
return -113 + 2 * csq;
}
uint8_t Sodaq_nbIOT::convertRSSI2CSQ(int8_t rssi) const {
return (rssi + 113) / 2;
}
bool Sodaq_nbIOT::startsWith(const char *pre, const char *str) {
return (strncmp(pre, str, strlen(pre)) == 0);
}
size_t Sodaq_nbIOT::ipToString(IP_t ip, char *buffer, size_t size) {
return snprintf(buffer, size, IP_FORMAT, IP_TO_TUPLE(ip));
}
bool Sodaq_nbIOT::isValidIPv4(const char *str) {
uint8_t segs = 0; // Segment count
uint8_t chcnt = 0; // Character count within segment
uint8_t accum = 0; // Accumulator for segment
if (!str) {
return false;
}
// Process every character in string
while (*str != '\0') {
// Segment changeover
if (*str == '.') {
// Must have some digits in segment
if (chcnt == 0) {
return false;
}
// Limit number of segments
if (++segs == 4) {
return false;
}
// Reset segment values and restart loop
chcnt = accum = 0;
str++;
continue;
}
// Check numeric
if ((*str < '0') || (*str > '9')) {
return false;
}
// Accumulate and check segment
if ((accum = accum * 10 + *str - '0') > 255) {
return false;
}
// Advance other segment specific stuff and continue loop
chcnt++;
str++;
}
// Check enough segments and enough characters in last segment
if (segs != 3) {
return false;
}
if (chcnt == 0) {
return false;
}
// Address OK
return true;
}
bool Sodaq_nbIOT::waitForSignalQuality(uint32_t timeout) {
uint32_t start = millis();
const int8_t minRSSI = getMinRSSI();
int8_t rssi;
uint8_t ber;
uint32_t delay_count = 500;
while (!is_timedout(start, timeout)) {
if (getRSSIAndBER(&rssi, &ber)) {
if (rssi != 0 && rssi >= minRSSI) {
_lastRSSI = rssi;
_CSQtime = (int32_t) (millis() - start) / 1000;
return true;
}
}
sodaq_wdt_safe_delay(delay_count);
// Next time wait a little longer, but not longer than 5 seconds
if (delay_count < 5000) {
delay_count += 1000;
}
}
return false;
}
ResponseTypes
Sodaq_nbIOT::_cgattParser(ResponseTypes &response, const char *buffer, size_t size, uint8_t *result, uint8_t *dummy) {
if (!result) {
return ResponseError;
}
int val;
if (sscanf(buffer, "+CGATT: %d", &val) == 1) {
*result = val;
return ResponseEmpty;
}
return ResponseError;
}
ResponseTypes Sodaq_nbIOT::_csqParser(ResponseTypes &response, const char *buffer, size_t size,
int *rssi, int *ber) {
if (!rssi || !ber) {
return ResponseError;
}
if (sscanf(buffer, "+CSQ: %d,%d", rssi, ber) == 2) {
return ResponseEmpty;
}
return ResponseError;
}
bool Sodaq_nbIOT::sendMessage(const uint8_t *buffer, size_t size) {
if (size > 512) {
return false;
}
print("AT+NMGS=");
print(size);
print(",");
for (uint16_t i = 0; i < size; ++i) {
print(static_cast<char>(NIBBLE_TO_HEX_CHAR(HIGH_NIBBLE(buffer[i]))));
print(static_cast<char>(NIBBLE_TO_HEX_CHAR(LOW_NIBBLE(buffer[i]))));
}
println();
return (readResponse() == ResponseOK);
}
int Sodaq_nbIOT::getSentMessagesCount(SentMessageStatus filter) {
println("AT+NQMGS");
uint16_t pendingCount = 0;
uint16_t errorCount = 0;
if (readResponse<uint16_t, uint16_t>(_nqmgsParser, &pendingCount, &errorCount) == ResponseOK) {
if (filter == Pending) {
return pendingCount;
} else if (filter == Error) {
return errorCount;
}
}
return -1;
}
ResponseTypes
Sodaq_nbIOT::_nqmgsParser(ResponseTypes &response, const char *buffer, size_t size, uint16_t *pendingCount,
uint16_t *errorCount) {
if (!pendingCount || !errorCount) {
return ResponseError;
}
int pendingValue;
int errorValue;
if (sscanf(buffer, "PENDING=%d,SENT=%*d,ERROR=%d", &pendingValue, &errorValue) == 2) {
*pendingCount = pendingValue;
*errorCount = errorValue;
return ResponseEmpty;
}
return ResponseError;
}
bool Sodaq_nbIOT::sendMessage(const char *str) {
return sendMessage((const uint8_t *) str, strlen(str));
}
bool Sodaq_nbIOT::sendMessage(String str) {
return sendMessage(str.c_str());
}
// ==============================
// on/off class
// ==============================
Sodaq_nbIotOnOff::Sodaq_nbIotOnOff() {
_onoffPin = -1;
_onoff_status = false;
}
// Initializes the instance
void Sodaq_nbIotOnOff::init(int onoffPin) {
if (onoffPin >= 0) {
_onoffPin = onoffPin;
// First write the output value, and only then set the output mode.
digitalWrite(_onoffPin, LOW);
pinMode(_onoffPin, OUTPUT);
}
}
void Sodaq_nbIotOnOff::on() {
if (_onoffPin >= 0) {
digitalWrite(_onoffPin, HIGH);
}
_onoff_status = true;
}
void Sodaq_nbIotOnOff::off() {
// The GPRSbee is switched off immediately
if (_onoffPin >= 0) {
digitalWrite(_onoffPin, LOW);
}
// Should be instant
// Let's wait a little, but not too long
delay(50);
_onoff_status = false;
}
bool Sodaq_nbIotOnOff::isOn() {
#if defined(ARDUINO_ARCH_AVR)
// Use the onoff pin, which is close to useless
bool status = digitalRead(_onoffPin);
return status;
#elif defined(ARDUINO_ARCH_SAMD)
// There is no status pin. On SAMD we cannot read back the onoff pin.
// So, our own status is all we have.
return _onoff_status;
#endif
// Let's assume it is on.
return true;
}