2026-08-04 14:23:07 +08:00

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
}