diff --git a/.gitignore b/.gitignore index 46f42f8f..ce5b7ea6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ install_manifest.txt compile_commands.json CTestTestfile.cmake _deps +build diff --git a/tools/rpi/discover/CMakeLists.txt b/tools/rpi/discover/CMakeLists.txt new file mode 100644 index 00000000..af3f27ee --- /dev/null +++ b/tools/rpi/discover/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.12) + +project(discover CXX) +add_compile_options(-Ofast -Wall) # passing the compiler a `-pthread` flag doesn't work here + +find_library(RF24 rf24 REQUIRED) +message(STATUS "using RF24 library: ${RF24}") + +add_executable(discover discover.cpp) +target_link_libraries(discover PUBLIC ${RF24} pthread) diff --git a/tools/rpi/discover/README.md b/tools/rpi/discover/README.md new file mode 100644 index 00000000..955a7d30 --- /dev/null +++ b/tools/rpi/discover/README.md @@ -0,0 +1,7 @@ +Try to discover inverters with known serial numbers. + +This tool will continuously scan all channels. + +Requires NRF24 library from https://github.com/nRF24/RF24/ and an NRF24 +module connected to the Raspberry Pi's GPIO header as described in +https://nrf24.github.io/RF24/index.html. diff --git a/tools/rpi/discover/discover.cpp b/tools/rpi/discover/discover.cpp new file mode 100644 index 00000000..85df891c --- /dev/null +++ b/tools/rpi/discover/discover.cpp @@ -0,0 +1,326 @@ +/* based on "gettingstarted.cpp" by 2bdy5 */ + +/** + * "PING" all known microinverters (serial numbers) on all + * known channels. + * Use a "known good" master (DTU) address. + * Keep track of the inverters as they frequency-hop. + * + * Test this tool by setting up an instance of "gettingstarted.cpp" + * with address setting '0' (default) as a test receiver. + */ +#include // time() +#include // cin, cout, endl +#include +#include // string, getline() +#include +#include +#include // CLOCK_MONOTONIC_RAW, timespec, clock_gettime() +#include // RF24, RF24_PA_LOW, delay() + +using namespace std; + +// Generic: +RF24 radio(22, 0); +/****************** Linux (BBB,x86,etc) ***********************/ +// See http://nRF24.github.io/RF24/pages.html for more information on usage +// See http://iotdk.intel.com/docs/master/mraa/ for more information on MRAA +// See https://www.kernel.org/doc/Documentation/spi/spidev for more information on SPIDEV + +// For this example, we'll be using a payload containing +// a single float number that will be incremented +// on every successful transmission +static union { +float payload = 0.0; +uint8_t b[4]; +}; + +void setRole(); // prototype to set the node's role +void master(); // prototype of the TX node's behavior +void slave(); // prototype of the RX node's behavior + +// custom defined timer for evaluating transmission time in microseconds +struct timespec startTimer, endTimer; +uint32_t getMicros(); // prototype to get ellapsed time in microseconds + + +/** Convert given 5-byte address to human readable hex string */ +string prettyPrintAddr(string &a) +{ + ostringstream o; + o << hex << setw(2) + << setfill('0') << setw(2) << int(a[0]) + << ":" << setw(2) << int(a[1]) + << ":" << setw(2) << int(a[2]) + << ":" << setw(2) << int(a[3]) + << ":" << setw(2) << int(a[4]) << dec; + return o.str(); +} + + +/** Convert a hoymiles inverter/DTU serial number into its + * corresponding NRF24 address byte sequence (5 bytes). + * + * The inverters use a BCD representation of the last 8 + * digits of the serial number, in reverse byte order, + * followed by a \x01. + */ +string serno2shockburstaddrbytes(uint64_t n) +{ + char b[5]; + b[3] = (((n/10)%10) << 4) | ((n/1)%10); + b[2] = (((n/1000)%10) << 4) | ((n/100)%10); + b[1] = (((n/100000)%10) << 4) | ((n/10000)%10); + b[0] = (((n/10000000)%10) << 4) | ((n/1000000)%10); + b[4] = 0x01; + + string s = string(b, sizeof(b)); + + cout << dec << "ser# " << n << " --> addr " + << prettyPrintAddr(s) << endl; + return s; +} + + +/** Ping the given address. + * @returns true if we received a reply, otherwise false. + */ +bool doPing(int ch, string src, string dst) +{ +// radio.setPayloadSize(sizeof(payload)); // float datatype occupies 4 bytes + radio.setPayloadSize(4); // float datatype occupies 4 bytes + radio.setChannel(ch); + + radio.setPALevel(RF24_PA_MIN); // RF24_PA_MAX is default. + radio.setDataRate(RF24_250KBPS); + + // set the TX address of the RX node into the TX pipe + radio.openWritingPipe((const uint8_t *)dst.c_str()); + + // set the RX address of the TX node into a RX pipe + radio.openReadingPipe(1, (const uint8_t *)src.c_str()); + + radio.stopListening(); // put radio in TX mode + + clock_gettime(CLOCK_MONOTONIC_RAW, &startTimer); // start the timer + // bool report = radio.write(&payload, sizeof(float)); // transmit & save the report + bool report = radio.write(&payload, 4); // transmit & save the report + uint32_t timerEllapsed = getMicros(); // end the timer + + if (report) { + // payload was delivered + payload += 0.01; // increment float payload + return true; + } + return false; // no reply received +} + + +int main(int argc, char** argv) +{ + if (!radio.begin()) { + cout << "radio hardware is not responding!!" << endl; + return 0; // quit now + } + + if(!radio.isPVariant()) + { + printf("not nRF24L01+\n"); + return 0; + } + + if(!radio.isChipConnected()) + { + printf("not connected\n"); + return 0; + } + + // TODO + // we probably want + // - 8-bit crc + // - dynamic payloads (check in rf logs) + // - what's the "primary mode"? + // - do we need/want "custom ack payloads"? + // - use isAckPayloadAvailable() once we've actually contacted an inverter successfully! + + radio.printPrettyDetails(); + + // well-known valid DTU serial number + // just in case the inverter only responds to addresses + // that fulfil certain requirements. + string masteraddr = serno2shockburstaddrbytes(99912345678); + + // serial numbers of all inverters that we are trying to find + vector dstaddrs; + dstaddrs.push_back(string("1Node")); + dstaddrs.push_back(string("2Node")); + dstaddrs.push_back(serno2shockburstaddrbytes(114174608145)); + dstaddrs.push_back(serno2shockburstaddrbytes(114174608177)); + + // channels that we will scan + vector channels{1, 3, 6, 9, 11, 23, 40, 61, 75, 76, 99}; + + for(auto & ch : channels) + { + cout << "ch " << setw(2) << ch << " "; + for(auto & a : dstaddrs) + { + cout << prettyPrintAddr(a); + bool success = doPing(ch, masteraddr, a); + if(success) { + cout << " XXX"; + } else { + cout << " - "; + } + cout << " " << flush; + delay(20); + } + cout << endl; + } + + radio.setChannel(76); + + // to use different addresses on a pair of radios, we need a variable to + // uniquely identify which address this radio will use to transmit + bool radioNumber = 1; // 0 uses address[0] to transmit, 1 uses address[1] to transmit + + // print example's name + cout << argv[0] << endl; + + // Let these addresses be used for the pair + uint8_t address[2][6] = {"1Node", "2Node"}; + // It is very helpful to think of an address as a path instead of as + // an identifying device destination + + // Set the radioNumber via the terminal on startup + cout << "Which radio is this? Enter '0' or '1'. Defaults to '0' "; + string input; + getline(cin, input); + radioNumber = input.length() > 0 && (uint8_t)input[0] == 49; + + // save on transmission time by setting the radio to only transmit the + // number of bytes we need to transmit a float + radio.setPayloadSize(sizeof(payload)); // float datatype occupies 4 bytes + + // Set the PA Level low to try preventing power supply related problems + // because these examples are likely run with nodes in close proximity to + // each other. + radio.setPALevel(RF24_PA_MIN); // RF24_PA_MAX is default. + radio.setDataRate(RF24_250KBPS); + +radio.printPrettyDetails(); + + // set the TX address of the RX node into the TX pipe + radio.openWritingPipe(address[radioNumber]); // always uses pipe 0 + + // set the RX address of the TX node into a RX pipe + radio.openReadingPipe(1, address[!radioNumber]); // using pipe 1 + + // For debugging info + // radio.printDetails(); // (smaller) function that prints raw register values + // radio.printPrettyDetails(); // (larger) function that prints human readable data + + // ready to execute program now + setRole(); // calls master() or slave() based on user input + return 0; +} + + +/** + * set this node's role from stdin stream. + * this only considers the first char as input. + */ +void setRole() { + string input = ""; + while (!input.length()) { + cout << "*** PRESS 'T' to begin transmitting to the other node\n"; + cout << "*** PRESS 'R' to begin receiving from the other node\n"; + cout << "*** PRESS 'Q' to exit" << endl; + getline(cin, input); + if (input.length() >= 1) { + if (input[0] == 'T' || input[0] == 't') + master(); + else if (input[0] == 'R' || input[0] == 'r') + slave(); + else if (input[0] == 'Q' || input[0] == 'q') + break; + else + cout << input[0] << " is an invalid input. Please try again." << endl; + } + input = ""; // stay in the while loop + } // while +} // setRole() + + +/** + * make this node act as the transmitter + */ +void master() { + radio.stopListening(); // put radio in TX mode + + unsigned int failure = 0; // keep track of failures + while (failure < 60) { + clock_gettime(CLOCK_MONOTONIC_RAW, &startTimer); // start the timer + bool report = radio.write(&payload, sizeof(float)); // transmit & save the report + uint32_t timerEllapsed = getMicros(); // end the timer + + if (report) { + // payload was delivered + cout << "Transmission successful! Time to transmit = "; + cout << timerEllapsed; // print the timer result + cout << " us. Sent: " << payload; // print payload sent + cout << " hex: " << hex << (unsigned int)b[0] << " " << (unsigned int)b[1] << " " + << (unsigned int)b[2] << " " << (unsigned int)b[3] << " " <