/* 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 . */ #include "Sodaq_nbIOT.h" #include #include #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 = 6; static NameValuePair nConfig[nConfigCount] = { { "AUTOCONNECT", "TRUE" }, { "CR_0354_0338_SCRAMBLING", "TRUE" }, { "CR_0859_SI_AVOID", "TRUE" }, { "COMBINE_ATTACH" , "FALSE" }, { "CELL_RESELECTION" , "FALSE" }, { "ENABLE_BIP" , "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); 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, 250); 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, "+NSONMI: %d,%d", ¶m1, ¶m2) == 2) { uint16_t socket_nr = param1; uint16_t nr_bytes = param2; if (socket_nr < ARRAY_SIZE(_socketPendingBytes)) { _socketPendingBytes[socket_nr] = nr_bytes; } continue; } //int param1, param2; //if (sscanf(buffer, "+UUSORD: %d,%d", ¶m1, ¶m2) == 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", ¶m1) == 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=\""); print(cdp); println("\""); return (readResponse() == ResponseOK); } void Sodaq_nbIOT::purgeAllResponsesRead() { uint32_t start = millis(); // make sure all the responses within the timeout have been read while ((readResponse(0, 1000) != ResponseTimeout) && !is_timedout(start, 2000)) {} } // 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; } purgeAllResponsesRead(); if (!setRadioActive(false)) { return false; } if (!checkAndApplyNconfig()) { return false; } reboot(); if (!on()) { return false; } purgeAllResponsesRead(); 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; } #ifdef DEBUG println("AT+CPSMS?"); readResponse(); readResponse(); #endif // 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(_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("\",\""); print(value); println("\""); 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, R"(+NCONFIG: "%[^"]","%[^"]")", 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)) { if (isConnected()) { return true; } //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; } purgeAllResponsesRead(); if (!setRadioActive(false)) { return false; } if (!checkAndApplyNconfig()) { return false; } reboot(); if (!on()) { return false; } purgeAllResponsesRead(); if (!setRadioActive(true)) { return false; } powerSaveOff(); println(R"(AT+COPS=1,2,"21630")"); readResponse(); while (!isRegistered()) { sodaq_wdt_safe_delay(3000); } debugPrintLn("REGISTRATION DONE"); return waitForSignalQuality(); } bool Sodaq_nbIOT::powerSaveOff() { println(R"(AT+CEDRXS=0,5,"0101")"); readResponse(); println(R"(AT+CPSMS=0,,,"01000001","00000101")"); return readResponse() == ResponseOK; } bool Sodaq_nbIOT::reconnectSocket() { println(R"(AT+COPS=1,2,"21630")"); readResponse(); while (!isRegistered()) { sodaq_wdt_safe_delay(3000); } debugPrintLn("REGISTRATION DONE"); return waitForSignalQuality(); } bool Sodaq_nbIOT::isRegistered() { bool registered = false; debugPrintLn("Checking registration state ... "); println("AT+CEREG?"); sodaq_wdt_safe_delay(AT_CMD_SLEEP_TIME); if (readResponse(_registeredParser, ®istered, NULL) != ResponseOK) { return false; } debugPrintLn(registered ? F("OK") : F("WAITING")) return registered; } ResponseTypes Sodaq_nbIOT::_registeredParser(ResponseTypes &response, const char *buffer, size_t size, bool *registered, uint8_t *dummy) { int value; if (sscanf(buffer, "+CEREG: %d,%d", NULL, &value) == 2) { *registered = (value == 1); return ResponseEmpty; } return ResponseError; } int Sodaq_nbIOT::createSocket(uint16_t localPort) { print(R"(AT+NSOCR="DGRAM",17,)"); print(localPort); println(",1"); // enable incoming message URC (NSONMI) delay(500); uint8_t socket; if (readResponse(_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, uint8_t *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(NIBBLE_TO_HEX_CHAR(HIGH_NIBBLE(buffer[i])))); print(static_cast(NIBBLE_TO_HEX_CHAR(LOW_NIBBLE(buffer[i])))); } println("\""); delay(AT_CMD_SLEEP_TIME); uint8_t sent; return ((readResponse(_sendSocketParser, &socket, &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; if (sscanf(buffer, "%*d,%d", &value) == 1) { *sent = value; 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); debugPrintLn(F(__FILE__": socketReceive() CMD")); uint8_t size_read = 0; if (readResponse(_socketReceiveParser, buffer, &size_read, &size) == ResponseOK) { debugPrintLn(__FILE__": socketReceive() OK"); _socketPendingBytes[socket] -= size_read; return size_read; } debugPrintLn(__FILE__": socketReceive() ERR"); return 0; } ResponseTypes Sodaq_nbIOT::_socketReceiveParser(ResponseTypes &response, const char *buffer, size_t size, char *parsedBuffer, uint8_t *size_read) { int length = 0; auto * resultBuffer = static_cast(malloc(250)); memset(resultBuffer, 0, 250); SerialUSB.println(__FILE__": _socketReceiveParser() sscanf"); /** * create a result buffer for the HEX string input * 250 is SODAQ_AT_DEVICE_DEFAULT_INPUT_BUFFER_SIZE defined in Sodaq_AT_Device.h * should probably refactor some more and move it to Sodaq_AT_Device.h * we probably only need half of this (+1) */ if (sscanf(buffer, R"(%*d,"%*[^"]",%*d,%d,"%[^"]",%*d)", &length, resultBuffer) == 2) { SerialUSB.print(__FILE__": length "); SerialUSB.println(length); for(int i=0; i < length;i++) { parsedBuffer[i] = HEX_PAIR_TO_BYTE(resultBuffer[i*2], resultBuffer[i*2+1]); } free(resultBuffer); SerialUSB.println(__FILE__": _socketReceiveParser() OK"); *size_read = length; return ResponseEmpty; } SerialUSB.println(__FILE__": _socketReceiveParser() ERR"); free(resultBuffer); return ResponseError; } size_t Sodaq_nbIOT::socketBytesPending(uint8_t socket) { readResponse(NULL, 250); return _socketPendingBytes[socket]; } 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(_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(_csqParser, &csqRaw, &berRaw) == ResponseOK) { *rssi = ((csqRaw == 99) ? 0 : convertCSQ2RSSI(csqRaw)); *ber = ((berRaw == 99 || static_cast(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(NIBBLE_TO_HEX_CHAR(HIGH_NIBBLE(buffer[i])))); print(static_cast(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(_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; }