Skip to content

Commit 5396000

Browse files
committed
fix(atik-efw): handle EFW2 status responses
Parse compact position/slot frames observed on hardware, skip FTDI status-only packets, and fall back safely when status remains unavailable.
1 parent 8a17f78 commit 5396000

4 files changed

Lines changed: 236 additions & 62 deletions

File tree

indi-atik-efw/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ find_package(USB1 REQUIRED)
1010
find_package(Threads REQUIRED)
1111

1212
set(ATIK_EFW_VERSION_MAJOR 1)
13-
set(ATIK_EFW_VERSION_MINOR 0)
13+
set(ATIK_EFW_VERSION_MINOR 1)
1414

1515
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h)
1616
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/indi_atik_efw.xml.cmake ${CMAKE_CURRENT_BINARY_DIR}/indi_atik_efw.xml)

indi-atik-efw/atik_efw.cpp

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "config.h"
2626

2727
#include <algorithm>
28+
#include <chrono>
2829
#include <deque>
2930
#include <iomanip>
3031
#include <memory>
@@ -53,9 +54,11 @@ constexpr uint16_t kFtdiFlowValue = 0x0303;
5354
constexpr unsigned int kControlTimeoutMs = 1000;
5455
constexpr unsigned int kReadTimeoutMs = 1000;
5556
constexpr unsigned int kWriteTimeoutMs = 1000;
57+
constexpr int kMaxStatusReadAttempts = 4;
5658
constexpr useconds_t kUsbResetDelayUs = 1000000;
5759
constexpr useconds_t kFtdiDelayUs = 200000;
5860
constexpr useconds_t kStatusDelayUs = 100000;
61+
constexpr std::chrono::milliseconds kMoveFallbackDelay {1500};
5962
constexpr int kDefaultSlots = 5;
6063
constexpr int kMaxSlots = 16;
6164
constexpr int kMaxResponseBytes = 64;
@@ -75,6 +78,9 @@ std::string bytesToHex(const std::vector<uint8_t> &data)
7578

7679
std::vector<uint8_t> sanitizeResponse(const std::vector<uint8_t> &raw)
7780
{
81+
if (raw.size() == 2)
82+
return {};
83+
7884
if (raw.size() >= 3 && raw[0] != kFrameByte && raw[2] == kFrameByte)
7985
return {raw.begin() + 2, raw.end()};
8086

@@ -90,16 +96,17 @@ int decodeByte(uint8_t value)
9096

9197
bool parseStatusResponse(const std::vector<uint8_t> &data, int *position, int *slots)
9298
{
99+
// Some wheel firmware returns one frame containing the status command and values.
93100
auto start = data.begin();
94101
while (start != data.end())
95102
{
96103
start = std::find(start, data.end(), kFrameByte);
97104
if (start == data.end())
98-
return false;
105+
break;
99106

100107
auto end = std::find(start + 1, data.end(), kFrameByte);
101108
if (end == data.end())
102-
return false;
109+
break;
103110

104111
if (end - start >= 3 && *(start + 1) == kCmdStatus)
105112
{
@@ -120,7 +127,37 @@ bool parseStatusResponse(const std::vector<uint8_t> &data, int *position, int *s
120127
start = end + 1;
121128
}
122129

123-
return false;
130+
// EFW2 hardware also returns two compact frames: #position##slot-count#.
131+
std::vector<int> values;
132+
start = data.begin();
133+
while (start != data.end())
134+
{
135+
start = std::find(start, data.end(), kFrameByte);
136+
if (start == data.end())
137+
break;
138+
139+
auto end = std::find(start + 1, data.end(), kFrameByte);
140+
if (end == data.end())
141+
break;
142+
143+
if (end - start == 2)
144+
{
145+
int value = decodeByte(*(start + 1));
146+
if (value > 0 && value <= kMaxSlots)
147+
values.push_back(value);
148+
}
149+
150+
start = end + 1;
151+
}
152+
153+
if (values.empty())
154+
return false;
155+
156+
if (position)
157+
*position = values[0];
158+
if (slots && values.size() > 1)
159+
*slots = values[1];
160+
return true;
124161
}
125162

126163
bool writeCommand(AtikEfwUsb::DeviceHandle &handle, const std::vector<uint8_t> &command)
@@ -129,18 +166,55 @@ bool writeCommand(AtikEfwUsb::DeviceHandle &handle, const std::vector<uint8_t> &
129166
return (rc == static_cast<int>(command.size()));
130167
}
131168

132-
bool readResponse(AtikEfwUsb::DeviceHandle &handle, std::vector<uint8_t> *response)
169+
enum class StatusReadResult
133170
{
134-
if (!response)
135-
return false;
171+
NoResponse,
172+
Unparsed,
173+
Parsed
174+
};
136175

137-
unsigned char buffer[kMaxResponseBytes] = {0};
138-
int rc = handle.read(kEndpointIn, buffer, sizeof(buffer), kReadTimeoutMs);
139-
if (rc <= 0)
140-
return false;
176+
StatusReadResult readStatusResponse(AtikEfwUsb::DeviceHandle &handle, std::vector<uint8_t> *rawResponse,
177+
int *position, int *slots)
178+
{
179+
std::vector<uint8_t> raw;
180+
std::vector<uint8_t> cleaned;
181+
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(kReadTimeoutMs);
141182

142-
response->assign(buffer, buffer + rc);
143-
return true;
183+
for (int attempt = 0; attempt < kMaxStatusReadAttempts; attempt++)
184+
{
185+
auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
186+
deadline - std::chrono::steady_clock::now()).count();
187+
if (remaining <= 0)
188+
break;
189+
190+
unsigned char buffer[kMaxResponseBytes] = {0};
191+
int rc = handle.read(kEndpointIn, buffer, sizeof(buffer), static_cast<unsigned int>(remaining));
192+
if (rc <= 0)
193+
break;
194+
195+
std::vector<uint8_t> packet(buffer, buffer + rc);
196+
raw.insert(raw.end(), packet.begin(), packet.end());
197+
198+
auto payload = sanitizeResponse(packet);
199+
cleaned.insert(cleaned.end(), payload.begin(), payload.end());
200+
201+
int parsedPosition = 0;
202+
int parsedSlots = 0;
203+
if (parseStatusResponse(cleaned, &parsedPosition, &parsedSlots))
204+
{
205+
if (rawResponse)
206+
*rawResponse = std::move(raw);
207+
if (position)
208+
*position = parsedPosition;
209+
if (slots)
210+
*slots = parsedSlots;
211+
return StatusReadResult::Parsed;
212+
}
213+
}
214+
215+
if (rawResponse)
216+
*rawResponse = raw;
217+
return raw.empty() ? StatusReadResult::NoResponse : StatusReadResult::Unparsed;
144218
}
145219

146220
bool initializeWheel(AtikEfwUsb::DeviceHandle &handle, std::string *error)
@@ -199,27 +273,18 @@ bool probeStatus(AtikEfwUsb::DeviceHandle &handle, bool requireParse, int *slotC
199273

200274
usleep(kStatusDelayUs);
201275

202-
std::vector<uint8_t> response;
203-
if (!readResponse(handle, &response))
204-
return false;
205-
206-
if (raw)
207-
*raw = response;
208-
209-
auto cleaned = sanitizeResponse(response);
210276
int slots = 0;
211277
int position = 0;
212-
bool parsed = parseStatusResponse(cleaned, &position, &slots);
213-
214-
if (parsed)
278+
auto result = readStatusResponse(handle, raw, &position, &slots);
279+
if (result == StatusReadResult::Parsed)
215280
{
216281
if (slotCount)
217282
*slotCount = slots;
218283
if (currentSlot)
219284
*currentSlot = position;
220285
}
221286

222-
return parsed || !requireParse;
287+
return result == StatusReadResult::Parsed || (!requireParse && result == StatusReadResult::Unparsed);
223288
}
224289

225290
std::string buildDeviceName(const AtikEfwUsb::DeviceInfo &info, size_t index, size_t count)
@@ -402,24 +467,6 @@ bool AtikEFW::sendCommand(const std::vector<uint8_t> &command)
402467
return true;
403468
}
404469

405-
bool AtikEFW::readResponse(std::vector<uint8_t> *response)
406-
{
407-
if (!handle_ || !response)
408-
return false;
409-
410-
unsigned char buffer[kMaxResponseBytes] = {0};
411-
int rc = handle_->read(kEndpointIn, buffer, sizeof(buffer), kReadTimeoutMs);
412-
if (rc <= 0)
413-
{
414-
LOGF_WARN("%s: no response (%d)", getDeviceName(), rc);
415-
return false;
416-
}
417-
418-
response->assign(buffer, buffer + rc);
419-
LOGF_DEBUG("%s: raw response %s", getDeviceName(), bytesToHex(*response).c_str());
420-
return true;
421-
}
422-
423470
bool AtikEFW::sendStatus(bool requireParse, int *slotCount, int *currentSlot)
424471
{
425472
const std::vector<uint8_t> statusCommand {kFrameByte, kCmdStatus, 0x00, kFrameByte};
@@ -430,17 +477,19 @@ bool AtikEFW::sendStatus(bool requireParse, int *slotCount, int *currentSlot)
430477
usleep(kStatusDelayUs);
431478

432479
std::vector<uint8_t> response;
433-
if (!readResponse(&response))
434-
return false;
435-
436-
auto cleaned = sanitizeResponse(response);
437480
int slots = 0;
438481
int position = 0;
439-
bool parsed = parseStatusResponse(cleaned, &position, &slots);
482+
auto result = readStatusResponse(*handle_, &response, &position, &slots);
483+
484+
if (!response.empty())
485+
LOGF_DEBUG("%s: raw response %s", getDeviceName(), bytesToHex(response).c_str());
440486

441-
if (!parsed)
487+
if (result != StatusReadResult::Parsed)
442488
{
443-
LOGF_WARN("%s: unparsed status response %s", getDeviceName(), bytesToHex(response).c_str());
489+
if (result == StatusReadResult::NoResponse)
490+
LOGF_WARN("%s: no status response", getDeviceName());
491+
else
492+
LOGF_WARN("%s: unparsed status response %s", getDeviceName(), bytesToHex(response).c_str());
444493
return !requireParse;
445494
}
446495

@@ -486,6 +535,8 @@ void AtikEFW::applySlotCount(int slots, bool updateProperty)
486535

487536
bool AtikEFW::Connect()
488537
{
538+
movementPending_ = false;
539+
489540
if (isSimulation())
490541
{
491542
int slots = slotCountHint_ > 0 ? slotCountHint_ : kDefaultSlots;
@@ -516,7 +567,7 @@ bool AtikEFW::Connect()
516567
int detectedPosition = 0;
517568
if (!sendStatus(false, &detectedSlots, &detectedPosition))
518569
{
519-
LOGF_ERROR("%s: no status response, aborting connection", getDeviceName());
570+
LOGF_ERROR("%s: failed to send status command", getDeviceName());
520571
handle_.reset();
521572
return false;
522573
}
@@ -530,6 +581,7 @@ bool AtikEFW::Connect()
530581
CurrentFilter = (detectedPosition > 0) ? detectedPosition : currentSlotHint_;
531582
if (CurrentFilter <= 0)
532583
CurrentFilter = 1;
584+
currentSlotHint_ = CurrentFilter;
533585

534586
applySlotCount(slots, true);
535587

@@ -546,6 +598,7 @@ bool AtikEFW::Connect()
546598

547599
bool AtikEFW::Disconnect()
548600
{
601+
movementPending_ = false;
549602
handle_.reset();
550603
return true;
551604
}
@@ -585,6 +638,8 @@ bool AtikEFW::SelectFilter(int targetFilter)
585638
return false;
586639
}
587640

641+
movementPending_ = true;
642+
movementStartedAt_ = std::chrono::steady_clock::now();
588643
return true;
589644
}
590645

@@ -601,6 +656,21 @@ int AtikEFW::QueryFilter()
601656
int position = 0;
602657
if (!sendStatus(true, &slots, &position))
603658
{
659+
if (movementPending_ && std::chrono::steady_clock::now() - movementStartedAt_ >= kMoveFallbackDelay)
660+
{
661+
LOGF_WARN("%s: status unavailable after filter change; assuming target slot %d",
662+
getDeviceName(), TargetFilter);
663+
CurrentFilter = TargetFilter;
664+
currentSlotHint_ = CurrentFilter;
665+
movementPending_ = false;
666+
FilterSlotNP[0].setValue(CurrentFilter);
667+
FilterSlotNP.apply();
668+
return CurrentFilter;
669+
}
670+
671+
if (movementPending_)
672+
return -1;
673+
604674
FilterSlotNP.setState(IPS_ALERT);
605675
FilterSlotNP.apply();
606676
return -1;
@@ -612,6 +682,9 @@ int AtikEFW::QueryFilter()
612682
if (position > 0)
613683
{
614684
CurrentFilter = position;
685+
currentSlotHint_ = CurrentFilter;
686+
if (CurrentFilter == TargetFilter)
687+
movementPending_ = false;
615688
FilterSlotNP[0].setValue(CurrentFilter);
616689
FilterSlotNP.apply();
617690
return CurrentFilter;

indi-atik-efw/atik_efw.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#include <indifilterwheel.h>
2828

29+
#include <chrono>
2930
#include <memory>
3031
#include <string>
3132
#include <vector>
@@ -69,14 +70,15 @@ class AtikEFW : public INDI::FilterWheel
6970
bool configureDevice();
7071
bool sendStatus(bool requireParse, int *slotCount, int *currentSlot);
7172
bool sendCommand(const std::vector<uint8_t> &command);
72-
bool readResponse(std::vector<uint8_t> *response);
7373
void applySlotCount(int slots, bool updateProperty);
7474

7575
AtikEfwUsb::Backend &backend_;
7676
AtikEfwUsb::DeviceInfo deviceInfo_;
7777
std::unique_ptr<AtikEfwUsb::DeviceHandle> handle_;
7878
int slotCountHint_ {0};
7979
int currentSlotHint_ {0};
80+
bool movementPending_ {false};
81+
std::chrono::steady_clock::time_point movementStartedAt_;
8082

8183
INDI::PropertyNumber SlotCountNP {1};
8284
};

0 commit comments

Comments
 (0)