292 lines
9.8 KiB
C++
292 lines
9.8 KiB
C++
#include "GpioInput.h"
|
|
|
|
#include <atomic>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
namespace {
|
|
|
|
volatile std::sig_atomic_t g_stopRequested = 0;
|
|
|
|
struct TestOptions
|
|
{
|
|
GpioInputConfig gpio;
|
|
int runSeconds = 30;
|
|
int printIntervalMs = 500;
|
|
};
|
|
|
|
void HandleSignal(int)
|
|
{
|
|
g_stopRequested = 1;
|
|
}
|
|
|
|
bool ParseInteger(const char* text,
|
|
long long minimum,
|
|
long long maximum,
|
|
long long& value)
|
|
{
|
|
if (!text || *text == '\0') {
|
|
return false;
|
|
}
|
|
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
const long long parsed = std::strtoll(text, &end, 10);
|
|
if (errno != 0 || end == text || *end != '\0' ||
|
|
parsed < minimum || parsed > maximum) {
|
|
return false;
|
|
}
|
|
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
void PrintUsage(const char* program)
|
|
{
|
|
std::cout
|
|
<< "Usage: " << program << " [options]\n\n"
|
|
<< "Options:\n"
|
|
<< " --direction <path> GPIO direction file\n"
|
|
<< " --value <path> GPIO value file\n"
|
|
<< " --seconds <n> Run duration (default: 30, 0 until Ctrl+C)\n"
|
|
<< " --active-high Physical high is active (default)\n"
|
|
<< " --active-low Physical low is active\n"
|
|
<< " --poll-ms <n> Poll interval (default: 10)\n"
|
|
<< " --debounce-ms <n> Debounce time (default: 50)\n"
|
|
<< " --reconnect-ms <n> Reconnect interval (default: 1000)\n"
|
|
<< " --print-ms <n> IO3 state print interval (default: 500)\n"
|
|
<< " -h, --help Show this help\n\n"
|
|
<< "Example:\n"
|
|
<< " " << program << " --seconds 30\n";
|
|
}
|
|
|
|
bool ReadOptionValue(int argc,
|
|
char* argv[],
|
|
int& index,
|
|
const std::string& option,
|
|
const char*& value)
|
|
{
|
|
if (index + 1 >= argc) {
|
|
std::cerr << "Missing value for " << option << '\n';
|
|
return false;
|
|
}
|
|
value = argv[++index];
|
|
return true;
|
|
}
|
|
|
|
bool ParseOptions(int argc, char* argv[], TestOptions& options)
|
|
{
|
|
for (int i = 1; i < argc; ++i) {
|
|
const std::string option = argv[i];
|
|
const char* text = nullptr;
|
|
long long value = 0;
|
|
|
|
if (option == "-h" || option == "--help") {
|
|
PrintUsage(argv[0]);
|
|
return false;
|
|
}
|
|
if (option == "--direction") {
|
|
if (!ReadOptionValue(argc, argv, i, option, text)) {
|
|
return false;
|
|
}
|
|
options.gpio.directionPath = text;
|
|
} else if (option == "--value") {
|
|
if (!ReadOptionValue(argc, argv, i, option, text)) {
|
|
return false;
|
|
}
|
|
options.gpio.valuePath = text;
|
|
} else if (option == "--seconds") {
|
|
if (!ReadOptionValue(argc, argv, i, option, text) ||
|
|
!ParseInteger(text, 0, 86400, value)) {
|
|
std::cerr << "Invalid run duration\n";
|
|
return false;
|
|
}
|
|
options.runSeconds = static_cast<int>(value);
|
|
} else if (option == "--active-low") {
|
|
options.gpio.activeLow = true;
|
|
} else if (option == "--active-high") {
|
|
options.gpio.activeLow = false;
|
|
} else if (option == "--poll-ms") {
|
|
if (!ReadOptionValue(argc, argv, i, option, text) ||
|
|
!ParseInteger(text, 1, 60000, value)) {
|
|
std::cerr << "Invalid poll interval\n";
|
|
return false;
|
|
}
|
|
options.gpio.pollIntervalMs = static_cast<int>(value);
|
|
} else if (option == "--debounce-ms") {
|
|
if (!ReadOptionValue(argc, argv, i, option, text) ||
|
|
!ParseInteger(text, 0, 60000, value)) {
|
|
std::cerr << "Invalid debounce time\n";
|
|
return false;
|
|
}
|
|
options.gpio.debounceMs = static_cast<int>(value);
|
|
} else if (option == "--reconnect-ms") {
|
|
if (!ReadOptionValue(argc, argv, i, option, text) ||
|
|
!ParseInteger(text, 100, 600000, value)) {
|
|
std::cerr << "Invalid reconnect interval\n";
|
|
return false;
|
|
}
|
|
options.gpio.reconnectIntervalMs = static_cast<int>(value);
|
|
} else if (option == "--print-ms") {
|
|
if (!ReadOptionValue(argc, argv, i, option, text) ||
|
|
!ParseInteger(text, 20, 60000, value)) {
|
|
std::cerr << "Invalid state print interval\n";
|
|
return false;
|
|
}
|
|
options.printIntervalMs = static_cast<int>(value);
|
|
} else {
|
|
std::cerr << "Unknown option: " << option << '\n';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (options.gpio.directionPath.empty()) {
|
|
std::cerr << "GPIO direction path cannot be empty\n";
|
|
return false;
|
|
}
|
|
if (options.gpio.valuePath.empty()) {
|
|
std::cerr << "GPIO value path cannot be empty\n";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
#if !defined(__linux__)
|
|
(void)argc;
|
|
(void)argv;
|
|
std::cerr << "GpioModuleTest only supports the Linux GPIO sysfs interface\n";
|
|
return 3;
|
|
#else
|
|
for (int i = 1; i < argc; ++i) {
|
|
const std::string option = argv[i];
|
|
if (option == "-h" || option == "--help") {
|
|
PrintUsage(argv[0]);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
TestOptions options;
|
|
if (!ParseOptions(argc, argv, options)) {
|
|
return 1;
|
|
}
|
|
|
|
std::signal(SIGINT, HandleSignal);
|
|
std::signal(SIGTERM, HandleSignal);
|
|
|
|
std::mutex outputMutex;
|
|
std::atomic<bool> everAvailable{false};
|
|
std::atomic<unsigned int> valueEventCount{0};
|
|
std::atomic<int> lastLogicalValue{-1};
|
|
const auto startedAt = std::chrono::steady_clock::now();
|
|
|
|
const auto elapsedMilliseconds = [&startedAt]() {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - startedAt).count();
|
|
};
|
|
const auto printLine = [&outputMutex, &elapsedMilliseconds](
|
|
const char* category,
|
|
const std::string& message) {
|
|
std::lock_guard<std::mutex> lock(outputMutex);
|
|
const double seconds =
|
|
static_cast<double>(elapsedMilliseconds()) / 1000.0;
|
|
std::cout << '[' << std::fixed << std::setprecision(3)
|
|
<< seconds << "s] " << category << ": "
|
|
<< message << std::endl;
|
|
};
|
|
|
|
std::cout << "GpioModule hardware test\n"
|
|
<< " direction : " << options.gpio.directionPath << '\n'
|
|
<< " value : " << options.gpio.valuePath << '\n'
|
|
<< " polarity : "
|
|
<< (options.gpio.activeLow ? "active-low" : "active-high")
|
|
<< '\n'
|
|
<< " poll : " << options.gpio.pollIntervalMs << " ms\n"
|
|
<< " debounce : " << options.gpio.debounceMs << " ms\n"
|
|
<< " print : " << options.printIntervalMs << " ms\n"
|
|
<< " duration : "
|
|
<< (options.runSeconds == 0
|
|
? std::string("until Ctrl+C")
|
|
: std::to_string(options.runSeconds) + " s")
|
|
<< "\n\nThe emergency-stop loop is normally closed: I/O3 is connected to GND.\n"
|
|
<< "Closed/GND must be INACTIVE; pressed or disconnected must be ACTIVE.\n";
|
|
|
|
GpioInput input;
|
|
const bool started = input.Start(
|
|
options.gpio,
|
|
[&printLine, &lastLogicalValue, &valueEventCount](bool active) {
|
|
const int logicalValue = active ? 1 : 0;
|
|
const int previous = lastLogicalValue.exchange(logicalValue);
|
|
valueEventCount.fetch_add(1);
|
|
std::string message = active ? "ACTIVE (logical 1)"
|
|
: "INACTIVE (logical 0)";
|
|
if (previous < 0) {
|
|
message += " [initial]";
|
|
} else if (previous == logicalValue) {
|
|
message += " [reconnected]";
|
|
} else {
|
|
message += " [changed]";
|
|
}
|
|
printLine("VALUE", message);
|
|
},
|
|
[&printLine, &everAvailable](bool available,
|
|
const std::string& message) {
|
|
if (available) {
|
|
everAvailable.store(true);
|
|
}
|
|
printLine(available ? "READY" : "ERROR", message);
|
|
});
|
|
|
|
if (!started) {
|
|
std::cerr << "Failed to start GpioInput: "
|
|
<< input.LastError() << '\n';
|
|
return 2;
|
|
}
|
|
|
|
const auto deadline = startedAt +
|
|
std::chrono::seconds(options.runSeconds);
|
|
auto nextStatePrint = std::chrono::steady_clock::now();
|
|
while (!g_stopRequested &&
|
|
(options.runSeconds == 0 ||
|
|
std::chrono::steady_clock::now() < deadline)) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now >= nextStatePrint) {
|
|
if (!input.IsAvailable()) {
|
|
printLine("IO3", "UNAVAILABLE");
|
|
} else {
|
|
printLine("IO3", input.IsActive()
|
|
? "ACTIVE (logical 1)"
|
|
: "INACTIVE (logical 0)");
|
|
}
|
|
nextStatePrint = now +
|
|
std::chrono::milliseconds(options.printIntervalMs);
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
|
|
input.Stop();
|
|
std::cout << "\nTest summary\n"
|
|
<< " GPIO available : "
|
|
<< (everAvailable.load() ? "yes" : "no") << '\n'
|
|
<< " value callbacks: " << valueEventCount.load() << '\n';
|
|
|
|
if (!everAvailable.load()) {
|
|
std::cerr << "GPIO never became available. Last error: "
|
|
<< input.LastError() << '\n';
|
|
return 2;
|
|
}
|
|
return 0;
|
|
#endif
|
|
}
|