Skip to content

Commit 3fd0e29

Browse files
authored
Refactor: Implement concurrent multi-factory device discovery (#1083)
This commit refactors the device discovery mechanism to support concurrent, non-blocking validation of multiple ports across multiple device factories. The key changes include: - **`DeviceManager`:** `discoverDevices` is completely rewritten to perform concurrent validation. It uses a round-robin scheduler to give each factory fair time on each port, significantly speeding up discovery when multiple ports or factories (like serial, TCP, mDNS) are present. The total discovery time is now bounded by the longest single-factory timeout, rather than the sum of all timeouts. - **`DeviceFactory`:** A new three-phase validation protocol (`beginValidation`, `stepValidation`, `completeValidation`) is introduced. This allows `DeviceManager` to drive the discovery process in a non-blocking manner. The existing `locateDevice` method is reimplemented as a blocking wrapper around this new protocol for single-port discovery. - **`cltool`:** - Added a `-use-mdns` command-line option to enable network device discovery via mDNS. - **Firmware Updater:** The post-update process is improved to more robustly wait for the device to reboot into application mode, with better progress reporting and timeout handling. - **Other:** Data types for message statistics in `message_stats.h` have been changed from `int` to `uint64_t` to prevent potential overflow.
1 parent dedd9e8 commit 3fd0e29

File tree

12 files changed

+498
-70
lines changed

12 files changed

+498
-70
lines changed

cltool/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ include_directories(
1818
# Link the InertialSenseSDK static library
1919
link_directories(${IS_SDK_DIR})
2020

21+
add_definitions(-DIS_LOG_LEVEL=IS_LOG_LEVEL_MORE_INFO)
22+
2123
if(WIN32)
2224
# yamlcpp needs to know it's a static lib
2325
add_definitions(-DYAML_CPP_STATIC_DEFINE)

cltool/src/cltool.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,10 @@ bool cltool_parseCommandLine(int argc, char* argv[])
658658
g_commandLineOptions.list_devices = true;
659659
g_commandLineOptions.displayMode = cInertialSenseDisplay::DMODE_QUIET;
660660
}
661+
else if (startsWith(a, "-use-mdns"))
662+
{
663+
g_commandLineOptions.useMdns = true;
664+
}
661665
else if (startsWith(a, "-lmf="))
662666
{
663667
g_commandLineOptions.maxLogFileSize = (uint32_t)strtoul(&a[5], NULL, 10);
@@ -927,6 +931,9 @@ bool cltool_parseCommandLine(int argc, char* argv[])
927931
}
928932
}
929933

934+
// Always apply the verboseLevel to the SDK log system
935+
IS_SET_LOG_LEVEL((eLogLevel)g_commandLineOptions.verboseLevel);
936+
930937
// We are either using a serial port or replaying data
931938
if ((g_commandLineOptions.comPort.length() == 0) && !g_commandLineOptions.replayDataLog)
932939
{
@@ -1175,6 +1182,7 @@ void cltool_outputUsage()
11751182
cout << " -dboc" << boldOff << " Send stop-broadcast command `$STPB` on close." << endlbOn;
11761183
cout << " -h --help" << boldOff << " Display this help menu." << endlbOn;
11771184
cout << " -list-devices" << boldOff << " Discovers and prints a list of discovered Inertial Sense devices and connected ports." << endlbOn;
1185+
cout << " -use-mdns" << boldOff << " Enable mDNS network discovery of remote devices (e.g., socat TCP-forwarded serial ports)." << endlbOn;
11781186
cout << " -lm" << boldOff << " Listen mode for ISB. Disables device verification (-vd) and does not send stop-broadcast command on start." << endlbOn;
11791187
cout << " -magRecal[n]" << boldOff << " Recalibrate magnetometers: 0=multi-axis, 1=single-axis" << endlbOn;
11801188
cout << " -nmea=[s]" << boldOff << " Send NMEA message s with added checksum footer. Display rx messages. (`-nmea=ASCE,0,GxGGA,1`)" << endlbOn;

cltool/src/cltool.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ typedef struct cmd_options_s // we need to name this to make MSVC happy, since w
176176
uint32_t updateFirmwareSlot = 0;
177177
uint32_t runDurationMs = 0; // Run for this many millis before exiting (0 = indefinitely)
178178
bool list_devices = false; // if true, dumps results of findDevices() including port name.
179+
bool useMdns = false; // if true, registers ISmDnsPortFactory for mDNS network device discovery
179180
EVFContainer_t evFCont = {0};
180181
EVMContainer_t evMCont = {0};
181182
EVOContainer_t evOCont;

cltool/src/cltool_main.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
4141
#include "CorrectionService.h"
4242
#include "NtripCorrectionService.h"
4343
#include "TcpPortFactory.h"
44+
#include "ISmDnsPortFactory.h"
4445
#include "util/natsort.h"
4546
#include "util/uri.hpp"
4647

@@ -956,13 +957,39 @@ void getMemoryEvent(InertialSense& inertialSenseInterface, uint32_t addrs, const
956957

957958
static int cltool_dataStreaming()
958959
{
960+
IS_LOG_OUTPUT(stdout);
961+
959962
// [C++ COMM INSTRUCTION] STEP 1: Instantiate InertialSense Class
960963
// Create InertialSense object, passing in data callback function pointer.
961-
InertialSense inertialSenseInterface({}, {&CltoolDeviceFactory::getInstance()});
964+
// Build explicit port factory list — only include ISmDnsPortFactory when -use-mdns is specified
965+
SerialPortFactory& spf = SerialPortFactory::getInstance();
966+
spf.portOptions.defaultBaudRate = g_commandLineOptions.baudRate;
967+
spf.portOptions.defaultBlocking = false;
968+
TcpPortFactory& tpf = TcpPortFactory::getInstance();
969+
tpf.portOptions.defaultBlocking = false;
970+
971+
std::vector<PortFactory*> portFactories = {&spf, &tpf};
972+
if (g_commandLineOptions.useMdns) {
973+
ISmDnsPortFactory& mdpf = ISmDnsPortFactory::getInstance();
974+
mdpf.portOptions.defaultBlocking = false;
975+
portFactories.push_back(&mdpf);
976+
}
977+
978+
InertialSense inertialSenseInterface(portFactories, {&CltoolDeviceFactory::getInstance()});
962979
g_inertialSenseInterface = &inertialSenseInterface;
963980
inertialSenseInterface.setErrorHandler(cltool_errorCallback);
964981
inertialSenseInterface.EnableDeviceValidation(!g_commandLineOptions.disableDeviceValidation);
965982

983+
// Pre-warm mDNS cache so network devices are discoverable when Open() calls discoverPorts()
984+
if (g_commandLineOptions.useMdns) {
985+
printf("Discovering mDNS devices...\n");
986+
uint32_t deadline = current_timeMs() + 3000;
987+
while (current_timeMs() < deadline) {
988+
ISmDnsPortFactory::tick();
989+
SLEEP_MS(50);
990+
}
991+
}
992+
966993
// [C++ COMM INSTRUCTION] STEP 2: Open serial port
967994
if (!inertialSenseInterface.Open(g_commandLineOptions.comPort.c_str(), g_commandLineOptions.baudRate, g_commandLineOptions.disableBroadcastsOnClose))
968995
{

src/DeviceFactory.cpp

Lines changed: 103 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @file DeviceFactory.cpp
2+
* @file DeviceFactory.cpp
33
* @brief ${BRIEF_DESC}
44
*
55
* @author Kyle Mallory on 3/10/25.
@@ -24,63 +24,120 @@ void DeviceFactory::locateDevices(std::function<bool(DeviceFactory*, const dev_i
2424
}
2525

2626
/**
27-
* Attempts to identify a specific type of device on the specified port, within the timeout period.
28-
* @param deviceCallback a function to be called if this Factory identified a possible/viable Inertial Sense device on the specified port
29-
* @param port the port to check for an Inertial Sense device. In most uses, the port specified should already be opened, however if the
30-
* port is not opened, this function will attempt to open it in order for ensure discovery.
31-
* @param hdwId a hardware Id qualifier that can be used to narrow the type of device. There is no direct indication that a hardware type
32-
* failed to match.
33-
* @param timeout the maximum time to attempt to identify a device before giving up. There is no direct indication that a timeout occurred.
34-
* @return true if a device was detected, otherwise false. Note that a false can result for any number of reasons, including invalid port,
35-
* hdwId mismatch, or a timeout.
27+
* Blocking single-port discovery. Implemented as a wrapper around the three-phase validation protocol.
3628
*/
3729
bool DeviceFactory::locateDevice(std::function<bool(DeviceFactory*, const dev_info_t&, port_handle_t)>& deviceCallback, port_handle_t port, uint16_t hdwId, uint16_t timeoutMs) {
38-
if (!portIsValid(port))
39-
return false; // TODO: Should we do anything special if the port is invalid? Really, we should never get here with an invalid port...
30+
// Check for existing device on this port
31+
device_handle_t dev = DeviceManager::getInstance().getDevice(port);
32+
if (dev) {
33+
if (dev->matchesHdwId(hdwId)) {
34+
if (!dev->port)
35+
dev->assignPort(port);
36+
return dev->validate(timeoutMs);
37+
}
38+
return false;
39+
}
40+
41+
// Phase 1: begin validation
42+
auto ctx = beginValidation(port, hdwId, timeoutMs);
43+
if (!ctx)
44+
return false;
45+
46+
// Phase 2: blocking loop
47+
while (!ctx->complete) {
48+
stepValidation(*ctx);
49+
if (!ctx->complete)
50+
SLEEP_MS(2);
51+
}
4052

41-
// can we open the port?
42-
if (!portIsOpened(port)) {
43-
log_debug(IS_LOG_DEVICE_FACTORY, "Opening serial port '%s'", portName(port));
53+
// Phase 3: complete validation
54+
dev_info_t devInfo;
55+
if (completeValidation(*ctx, devInfo)) {
56+
return deviceCallback(this, devInfo, port);
57+
}
58+
return false;
59+
}
60+
61+
/**
62+
* Phase 1: Opens the port, validates it, creates a base ISDevice for probing, and calls the
63+
* onBeginValidation() hook to allow the factory to decline or do custom setup.
64+
*/
65+
std::unique_ptr<DeviceFactory::ValidationContext> DeviceFactory::beginValidation(port_handle_t port, uint16_t hdwId, uint32_t timeoutMs, std::shared_ptr<ISDevice> sharedDevice) {
66+
if (!portIsValid(port)) {
67+
log_more_debug(IS_LOG_DEVICE_FACTORY, "beginValidation: port '%s' is invalid, skipping.", portName(port));
68+
return nullptr;
69+
}
70+
71+
// Open port if needed (only when no shared device provided, i.e. standalone/blocking path)
72+
if (!sharedDevice && !portIsOpened(port)) {
4473
if (!portValidate(port) || (portOpen(port) != PORT_ERROR__NONE)) {
45-
log_debug(IS_LOG_DEVICE_FACTORY, "Error opening serial port '%s'. Ignoring. Error was: %s", portName(port), SERIAL_PORT(port)->error);
46-
portClose(port); // failed to open
74+
portClose(port);
4775
portInvalidate(port);
48-
return false;
49-
// m_ignoredPorts.push_back(curPortName); // record this port name as bad, so we don't try and reopen it again
76+
return nullptr;
5077
}
5178
}
5279

53-
// We can only validate devices connected on COMM ports, since ISComm is needed to parse/communicate with the device
54-
if (!(portType(port) & PORT_TYPE__COMM))
55-
return false;
80+
// Only COMM ports can be validated via ISComm protocol
81+
if (!(portType(port) & PORT_TYPE__COMM)) {
82+
log_more_debug(IS_LOG_DEVICE_FACTORY, "beginValidation: port '%s' is not a COMM port (type=0x%04X), skipping.", portName(port), portType(port));
83+
return nullptr;
84+
}
5685

5786
if (timeoutMs <= 0)
5887
timeoutMs = deviceTimeout;
5988

60-
// at this point, the port should be opened...
61-
device_handle_t dev = DeviceManager::getInstance().getDevice(port);
62-
if (!dev) {
63-
// no previous device exists, so identify the device and then register it with the manager
64-
int validationResult = 0;
65-
ISDevice localDev(hdwId, port);
66-
do {
67-
is_comm_port_parse_messages(port); // Read data directly into comm buffer and call callback functions
68-
validationResult = localDev.validateAsync(timeoutMs);
69-
SLEEP_MS(2);
70-
} while (!validationResult);
89+
// Let the factory decline this port
90+
if (!onBeginValidation(port, hdwId)) {
91+
log_more_debug(IS_LOG_DEVICE_FACTORY, "beginValidation: factory declined port '%s'.", portName(port));
92+
return nullptr;
93+
}
7194

72-
if (localDev.hasDeviceInfo() && localDev.matchesHdwId(hdwId)) {
73-
// note that the deviceHandler callback can still reject the device for reasons
74-
return deviceCallback(this, localDev.devInfo, port);
75-
}
76-
} else if (dev->matchesHdwId(hdwId)) {
77-
if (!dev->port)
78-
dev->assignPort(port);
79-
else if (dev->port != port) {
80-
// FIXME: this isn't good - it shouldn't happen, but it might... we should deal with it.
81-
}
82-
// a device exists associated with this port already, there isn't anything to do.
83-
return dev->validate(timeoutMs);
95+
log_more_debug(IS_LOG_DEVICE_FACTORY, "beginValidation: created validation context for port '%s' (hdwId=0x%04X, timeout=%dms)", portName(port), hdwId, timeoutMs);
96+
auto ctx = std::make_unique<ValidationContext>();
97+
ctx->port = port;
98+
ctx->device = sharedDevice ? sharedDevice : std::make_shared<ISDevice>(hdwId, port);
99+
ctx->hdwId = hdwId;
100+
ctx->timeoutMs = timeoutMs;
101+
return ctx;
102+
}
103+
104+
/**
105+
* Phase 2: Reads pending data from the port, then calls the onStepValidation() hook to
106+
* advance validation state.
107+
*/
108+
int DeviceFactory::stepValidation(ValidationContext& ctx) {
109+
if (ctx.complete)
110+
return ctx.result;
111+
112+
is_comm_port_parse_messages(ctx.port);
113+
ctx.result = onStepValidation(*ctx.device, ctx.timeoutMs);
114+
ctx.complete = (ctx.result != 0);
115+
if (ctx.result == 1) {
116+
log_debug(IS_LOG_DEVICE_FACTORY, "stepValidation: port '%s' validated successfully.", portName(ctx.port));
117+
} else if (ctx.result == -1) {
118+
log_debug(IS_LOG_DEVICE_FACTORY, "stepValidation: port '%s' timed out.", portName(ctx.port));
84119
}
85-
return false;
120+
return ctx.result;
86121
}
122+
123+
/**
124+
* Phase 3: Checks that validation succeeded and the device has info, then calls the
125+
* onCompleteValidation() hook to let the factory accept or reject.
126+
*/
127+
bool DeviceFactory::completeValidation(ValidationContext& ctx, dev_info_t& devInfoOut) {
128+
if (ctx.result <= 0)
129+
return false;
130+
if (!ctx.device || !ctx.device->hasDeviceInfo()) {
131+
log_debug(IS_LOG_DEVICE_FACTORY, "completeValidation: port '%s' validated but has no device info.", portName(ctx.port));
132+
return false;
133+
}
134+
if (!onCompleteValidation(ctx.device->devInfo, ctx.hdwId)) {
135+
log_debug(IS_LOG_DEVICE_FACTORY, "completeValidation: factory rejected device on port '%s' (hdwId=0x%04X).", portName(ctx.port), ctx.hdwId);
136+
return false;
137+
}
138+
139+
devInfoOut = ctx.device->devInfo;
140+
log_debug(IS_LOG_DEVICE_FACTORY, "completeValidation: factory accepted device %s on port '%s'.",
141+
ISDevice::getIdAsString(devInfoOut).c_str(), portName(ctx.port));
142+
return true;
143+
}

src/DeviceFactory.h

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,30 @@
2222

2323
/**
2424
* An interface, to be implemented as a singleton, that is responsible for identifying various types of ISDevice and allocating and releasing them.
25+
*
26+
* Supports both blocking single-port discovery (locateDevice) and concurrent multi-port discovery
27+
* via a three-phase protocol (beginValidation / stepValidation / completeValidation) driven by
28+
* DeviceManager::discoverDevices(). Subclasses customize behavior by overriding the virtual hooks
29+
* (onBeginValidation, onStepValidation, onCompleteValidation) without managing ValidationContext.
2530
*/
2631
class DeviceFactory {
2732
public:
2833
virtual ~DeviceFactory() = default;
2934

35+
/**
36+
* Per-port, per-factory state during concurrent validation. Managed by the base class
37+
* beginValidation/stepValidation/completeValidation methods. Subclasses should not need
38+
* to interact with this directly — override the onXxx hooks instead.
39+
*/
40+
struct ValidationContext {
41+
port_handle_t port = nullptr;
42+
std::shared_ptr<ISDevice> device;
43+
uint16_t hdwId = IS_HARDWARE_ANY;
44+
uint32_t timeoutMs = 3000;
45+
bool complete = false;
46+
int result = 0; // -1 = failed/timeout, 0 = in-progress, 1 = success
47+
};
48+
3049
/**
3150
* A function to be implemented in the factory responsible for allocating the underlying device type and returning a pointer to it
3251
* This function should NOT manipulate the underlying port, such as opening, etc.
@@ -66,6 +85,7 @@ class DeviceFactory {
6685

6786
/**
6887
* Attempts to identify a specific type of device on the specified port, within the timeout period.
88+
* Implemented as a blocking wrapper around beginValidation/stepValidation/completeValidation.
6989
* @param deviceCallback a function to be called if this Factory identified a possible/viable Inertial Sense device on the specified port
7090
* @param port the port to check for an Inertial Sense device. In most uses, the port specified should already be opened, however if the
7191
* port is not opened, this function will attempt to open it in order for ensure discovery.
@@ -77,14 +97,71 @@ class DeviceFactory {
7797
*/
7898
virtual bool locateDevice(std::function<bool(DeviceFactory*, const dev_info_t&, port_handle_t)>& deviceCallback, port_handle_t port, uint16_t hdwId, uint16_t timeout);
7999

100+
/**
101+
* Phase 1: Prepare a ValidationContext for concurrent validation on this port.
102+
* If a shared ISDevice probe is provided, it will be used (concurrent discovery shares one
103+
* ISDevice per port across all factories). Otherwise, opens the port and creates a new probe.
104+
* Calls onBeginValidation() to allow the factory to decline or do custom setup.
105+
* @return a ValidationContext, or nullptr if this port cannot be validated
106+
*/
107+
std::unique_ptr<ValidationContext> beginValidation(port_handle_t port, uint16_t hdwId, uint32_t timeoutMs, std::shared_ptr<ISDevice> sharedDevice = nullptr);
108+
109+
/**
110+
* Phase 2: Non-blocking validation step. Reads pending data from the port, then calls
111+
* onStepValidation() to advance validation state. Called repeatedly during the concurrent
112+
* loop, only when this factory is the active round-robin slot for this port.
113+
* @return -1 = failed/timeout, 0 = still in progress, 1 = validated successfully
114+
*/
115+
int stepValidation(ValidationContext& ctx);
116+
117+
/**
118+
* Phase 3: After stepValidation returns 1, checks that the device has valid info and calls
119+
* onCompleteValidation() to let the factory accept or reject the device.
120+
* @param ctx the completed validation context
121+
* @param devInfoOut populated with the validated device info on success
122+
* @return true if this factory claims the device, false otherwise
123+
*/
124+
bool completeValidation(ValidationContext& ctx, dev_info_t& devInfoOut);
125+
80126
/**
81127
* Assigns a Factory-specific timeout period for each port
82128
* @param timeout
83129
*/
84130
void setPerDeviceTimeout(uint32_t timeout) { deviceTimeout = timeout; };
85131

132+
uint32_t getPerDeviceTimeout() const { return deviceTimeout; };
133+
134+
protected:
135+
/**
136+
* Virtual hook called during Phase 1. Override to decline certain ports or perform
137+
* custom port setup before validation begins.
138+
* @param port the port about to be validated
139+
* @param hdwId the hardware Id filter
140+
* @return true to proceed with validation, false to skip this port
141+
*/
142+
virtual bool onBeginValidation(port_handle_t port, uint16_t hdwId) { (void)port; (void)hdwId; return true; }
143+
144+
/**
145+
* Virtual hook called during Phase 2. Override to inject custom validation logic
146+
* (e.g., factory-specific queries or handshakes). The base class has already called
147+
* is_comm_port_parse_messages() before invoking this hook.
148+
* @param device the ISDevice probe to validate against
149+
* @param timeoutMs the timeout for this validation attempt
150+
* @return -1 = failed/timeout, 0 = still in progress, 1 = validated successfully
151+
*/
152+
virtual int onStepValidation(ISDevice& device, uint32_t timeoutMs) { return device.validateAsync(timeoutMs); }
153+
154+
/**
155+
* Virtual hook called during Phase 3. Override to accept or reject a validated device
156+
* based on factory-specific criteria (e.g., hardware type, firmware version).
157+
* @param devInfo the validated device info
158+
* @param hdwId the hardware Id filter from the discovery request
159+
* @return true to claim this device, false to pass to the next factory
160+
*/
161+
virtual bool onCompleteValidation(const dev_info_t& devInfo, uint16_t hdwId) { (void)devInfo; (void)hdwId; return true; }
162+
86163
private:
87-
uint32_t deviceTimeout = 3000;
164+
uint32_t deviceTimeout = 3000; // should match DeviceManager::DISCOVERY__DEFAULT_TIMEOUT
88165
};
89166

90167
class ImxDeviceFactory : public DeviceFactory {

0 commit comments

Comments
 (0)