@padosoft/react-native-ecr17 0.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/Ecr17.podspec +39 -39
  2. package/README.md +348 -348
  3. package/android/CMakeLists.txt +41 -41
  4. package/android/build.gradle +148 -148
  5. package/android/fix-prefab.gradle +50 -50
  6. package/android/gradle.properties +5 -5
  7. package/android/src/main/AndroidManifest.xml +2 -2
  8. package/android/src/main/cpp/cpp-adapter.cpp +8 -8
  9. package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -233
  10. package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -30
  11. package/cpp/Ecr17.hpp +1 -1
  12. package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -598
  13. package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -85
  14. package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -277
  15. package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -103
  16. package/cpp/Ecr17Response/Ecr17Response.cpp +155 -155
  17. package/cpp/Ecr17Response/Ecr17Response.hpp +113 -113
  18. package/cpp/Lcr/Lcr.cpp +42 -42
  19. package/cpp/Lcr/Lcr.hpp +21 -21
  20. package/cpp/PacketCodec/PacketCodec.cpp +145 -145
  21. package/cpp/PacketCodec/PacketCodec.hpp +47 -47
  22. package/cpp/Session/Ecr17Session.cpp +260 -260
  23. package/cpp/Session/Ecr17Session.hpp +97 -97
  24. package/cpp/Session/RetryPolicy.hpp +23 -23
  25. package/cpp/Transport/FakeTransport.hpp +95 -95
  26. package/cpp/Transport/NativeTransportAdapter.cpp +42 -42
  27. package/cpp/Transport/NativeTransportAdapter.hpp +32 -32
  28. package/cpp/Transport/Transport.hpp +30 -30
  29. package/cpp/tests/CMakeLists.txt +55 -55
  30. package/cpp/tests/PosixTcpTransport.hpp +105 -105
  31. package/cpp/tests/stubs/LrcMode.hpp +25 -25
  32. package/cpp/tests/test_flows.cpp +148 -148
  33. package/cpp/tests/test_integration_terminal.cpp +72 -72
  34. package/cpp/tests/test_lrc.cpp +66 -66
  35. package/cpp/tests/test_packet_codec.cpp +164 -164
  36. package/cpp/tests/test_protocol.cpp +102 -102
  37. package/cpp/tests/test_protocol_commands.cpp +190 -190
  38. package/cpp/tests/test_response.cpp +164 -164
  39. package/cpp/tests/test_retry_policy.cpp +28 -28
  40. package/cpp/tests/test_session.cpp +262 -262
  41. package/ios/HybridEcr17Transport.swift +103 -103
  42. package/lib/commonjs/index.js +50 -0
  43. package/lib/commonjs/index.js.map +1 -0
  44. package/lib/commonjs/package.json +1 -0
  45. package/lib/commonjs/specs/client.nitro.js +17 -0
  46. package/lib/commonjs/specs/client.nitro.js.map +1 -0
  47. package/lib/commonjs/specs/transport.nitro.js +6 -0
  48. package/lib/commonjs/specs/transport.nitro.js.map +1 -0
  49. package/lib/commonjs/types/client.js +2 -0
  50. package/lib/commonjs/types/client.js.map +1 -0
  51. package/lib/commonjs/utils/client.js +13 -0
  52. package/lib/commonjs/utils/client.js.map +1 -0
  53. package/lib/module/index.js +7 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/specs/client.nitro.js +13 -0
  56. package/lib/module/specs/client.nitro.js.map +1 -0
  57. package/lib/module/specs/transport.nitro.js +4 -0
  58. package/lib/module/specs/transport.nitro.js.map +1 -0
  59. package/lib/module/types/client.js +2 -0
  60. package/lib/module/types/client.js.map +1 -0
  61. package/lib/module/utils/client.js +9 -0
  62. package/lib/module/utils/client.js.map +1 -0
  63. package/lib/typescript/src/index.d.ts +5 -0
  64. package/lib/typescript/src/index.d.ts.map +1 -0
  65. package/lib/typescript/src/specs/client.nitro.d.ts +63 -0
  66. package/lib/typescript/src/specs/client.nitro.d.ts.map +1 -0
  67. package/lib/typescript/src/specs/transport.nitro.d.ts +13 -0
  68. package/lib/typescript/src/specs/transport.nitro.d.ts.map +1 -0
  69. package/lib/typescript/src/types/client.d.ts +138 -0
  70. package/lib/typescript/src/types/client.d.ts.map +1 -0
  71. package/lib/typescript/src/utils/client.d.ts +3 -0
  72. package/lib/typescript/src/utils/client.d.ts.map +1 -0
  73. package/nitro.json +30 -30
  74. package/package.json +4 -4
  75. package/react-native.config.js +18 -18
  76. package/src/index.ts +4 -4
  77. package/src/specs/client.nitro.ts +102 -102
  78. package/src/specs/transport.nitro.ts +25 -25
  79. package/src/types/client.ts +196 -196
  80. package/src/utils/client.ts +10 -10
@@ -1,48 +1,48 @@
1
- #pragma once
2
-
3
- #include <cstdint>
4
- #include <optional>
5
- #include <string>
6
- #include <vector>
7
-
8
- #include "Lcr/Lcr.hpp"
9
-
10
- namespace margelo::nitro::ecr17 {
11
-
12
- enum class PacketType {
13
- APPLICATION,
14
- // SOH-framed procedure progress update (0x01 ... 0x04), see protocol spec.
15
- PROGRESS,
16
- ACK,
17
- NAK,
18
- UNKNOWN,
19
- };
20
-
21
- struct DecodedPacket {
22
- PacketType type;
23
- std::string payload;
24
- bool validLrc;
25
- };
26
-
27
- class PacketCodec {
28
- public:
29
- static constexpr uint8_t STX = 0x02;
30
- static constexpr uint8_t ETX = 0x03;
31
- static constexpr uint8_t SOH = 0x01;
32
- static constexpr uint8_t EOT = 0x04;
33
- static constexpr uint8_t ACK = 0x06;
34
- static constexpr uint8_t NAK = 0x15;
35
-
36
- explicit PacketCodec(LrcMode mode);
37
-
38
- std::vector<uint8_t> encodeApplication(const std::string& payload);
39
-
40
- std::vector<uint8_t> encodeControl(uint8_t ctrl);
41
-
42
- DecodedPacket decode(const std::vector<uint8_t>& data);
43
-
44
- private:
45
- LrcMode lrcMode_;
46
- };
47
-
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <optional>
5
+ #include <string>
6
+ #include <vector>
7
+
8
+ #include "Lcr/Lcr.hpp"
9
+
10
+ namespace margelo::nitro::ecr17 {
11
+
12
+ enum class PacketType {
13
+ APPLICATION,
14
+ // SOH-framed procedure progress update (0x01 ... 0x04), see protocol spec.
15
+ PROGRESS,
16
+ ACK,
17
+ NAK,
18
+ UNKNOWN,
19
+ };
20
+
21
+ struct DecodedPacket {
22
+ PacketType type;
23
+ std::string payload;
24
+ bool validLrc;
25
+ };
26
+
27
+ class PacketCodec {
28
+ public:
29
+ static constexpr uint8_t STX = 0x02;
30
+ static constexpr uint8_t ETX = 0x03;
31
+ static constexpr uint8_t SOH = 0x01;
32
+ static constexpr uint8_t EOT = 0x04;
33
+ static constexpr uint8_t ACK = 0x06;
34
+ static constexpr uint8_t NAK = 0x15;
35
+
36
+ explicit PacketCodec(LrcMode mode);
37
+
38
+ std::vector<uint8_t> encodeApplication(const std::string& payload);
39
+
40
+ std::vector<uint8_t> encodeControl(uint8_t ctrl);
41
+
42
+ DecodedPacket decode(const std::vector<uint8_t>& data);
43
+
44
+ private:
45
+ LrcMode lrcMode_;
46
+ };
47
+
48
48
  } // namespace margelo::nitro::ecr17
@@ -1,260 +1,260 @@
1
- #include "Session/Ecr17Session.hpp"
2
-
3
- #include <algorithm>
4
- #include <chrono>
5
- #include <stdexcept>
6
- #include <thread>
7
-
8
- namespace margelo::nitro::ecr17 {
9
-
10
- using clock = std::chrono::steady_clock;
11
-
12
- Ecr17Session::Ecr17Session(Transport& transport, const SessionConfig& config)
13
- : transport_(transport), config_(config), codec_(config.lrcMode) {
14
- transport_.setDataCallback([this](const std::vector<uint8_t>& data) { onData(data); });
15
- transport_.setDisconnectCallback([this]() { onDisconnect(); });
16
- }
17
-
18
- void Ecr17Session::onData(const std::vector<uint8_t>& data) {
19
- {
20
- std::lock_guard<std::mutex> lock(mutex_);
21
- rxBuffer_.insert(rxBuffer_.end(), data.begin(), data.end());
22
- }
23
- cv_.notify_all();
24
- }
25
-
26
- void Ecr17Session::onDisconnect() {
27
- {
28
- std::lock_guard<std::mutex> lock(mutex_);
29
- disconnected_ = true;
30
- }
31
- cv_.notify_all();
32
- }
33
-
34
- // Extracts one complete frame from the front of rxBuffer_, dropping leading junk
35
- // bytes to resynchronise. Returns nullopt if no complete frame is available yet.
36
- // Caller must hold mutex_.
37
- std::optional<std::vector<uint8_t>> Ecr17Session::extractFrameLocked() {
38
- while (!rxBuffer_.empty()) {
39
- const uint8_t first = rxBuffer_.front();
40
-
41
- if (first == PacketCodec::ACK || first == PacketCodec::NAK) {
42
- if (rxBuffer_.size() < 3) {
43
- return std::nullopt; // wait for ETX + LRC
44
- }
45
- std::vector<uint8_t> frame(rxBuffer_.begin(), rxBuffer_.begin() + 3);
46
- rxBuffer_.erase(rxBuffer_.begin(), rxBuffer_.begin() + 3);
47
- return frame;
48
- }
49
-
50
- if (first == PacketCodec::STX) {
51
- auto etx = std::find(rxBuffer_.begin(), rxBuffer_.end(), PacketCodec::ETX);
52
- if (etx == rxBuffer_.end() || etx + 1 == rxBuffer_.end()) {
53
- return std::nullopt; // wait for ETX and the trailing LRC
54
- }
55
- auto lastByte = etx + 1; // LRC
56
- std::vector<uint8_t> frame(rxBuffer_.begin(), lastByte + 1);
57
- rxBuffer_.erase(rxBuffer_.begin(), lastByte + 1);
58
- return frame;
59
- }
60
-
61
- if (first == PacketCodec::SOH) {
62
- auto eot = std::find(rxBuffer_.begin(), rxBuffer_.end(), PacketCodec::EOT);
63
- if (eot == rxBuffer_.end()) {
64
- return std::nullopt; // wait for EOT
65
- }
66
- std::vector<uint8_t> frame(rxBuffer_.begin(), eot + 1);
67
- rxBuffer_.erase(rxBuffer_.begin(), eot + 1);
68
- return frame;
69
- }
70
-
71
- // Unrecognised lead byte: drop it and resynchronise.
72
- rxBuffer_.erase(rxBuffer_.begin());
73
- }
74
- return std::nullopt;
75
- }
76
-
77
- std::optional<DecodedPacket> Ecr17Session::waitForFrame(int timeoutMs) {
78
- std::unique_lock<std::mutex> lock(mutex_);
79
- const auto deadline = clock::now() + std::chrono::milliseconds(timeoutMs);
80
- while (true) {
81
- if (auto frame = extractFrameLocked()) {
82
- return codec_.decode(*frame);
83
- }
84
- if (disconnected_) {
85
- throw std::runtime_error("ECR17: transport disconnected during exchange");
86
- }
87
- if (cv_.wait_until(lock, deadline) == std::cv_status::timeout) {
88
- if (auto frame = extractFrameLocked()) {
89
- return codec_.decode(*frame);
90
- }
91
- return std::nullopt;
92
- }
93
- }
94
- }
95
-
96
- void Ecr17Session::sendControl(uint8_t control) {
97
- transport_.send(codec_.encodeControl(control));
98
- }
99
-
100
- bool Ecr17Session::isReceipt(const std::string& payload) {
101
- // Send-ticket message from the terminal uses message code 'S' at position 10.
102
- return payload.size() >= 10 && payload[9] == 'S';
103
- }
104
-
105
- void Ecr17Session::resetForNewTransaction() {
106
- std::lock_guard<std::mutex> lock(mutex_);
107
- disconnected_ = false;
108
- rxBuffer_.clear();
109
- pendingResult_.reset();
110
- }
111
-
112
- void Ecr17Session::sendAckOnly(const std::string& requestPayload) {
113
- resetForNewTransaction();
114
- ackHandshake(requestPayload);
115
- }
116
-
117
- void Ecr17Session::ackHandshake(const std::string& requestPayload) {
118
- const std::vector<uint8_t> requestFrame = codec_.encodeApplication(requestPayload);
119
-
120
- transport_.send(requestFrame);
121
- int attempts = 1;
122
- auto deadline = clock::now() + std::chrono::milliseconds(config_.ackTimeoutMs);
123
-
124
- while (true) {
125
- const auto remaining =
126
- std::chrono::duration_cast<std::chrono::milliseconds>(deadline - clock::now()).count();
127
- if (remaining <= 0) {
128
- if (attempts > config_.retryCount) {
129
- throw std::runtime_error("ECR17: no ACK after " + std::to_string(attempts) +
130
- " attempts");
131
- }
132
- std::this_thread::sleep_for(std::chrono::milliseconds(config_.retryDelayMs));
133
- transport_.send(requestFrame);
134
- ++attempts;
135
- deadline = clock::now() + std::chrono::milliseconds(config_.ackTimeoutMs);
136
- continue;
137
- }
138
-
139
- std::optional<DecodedPacket> pkt = waitForFrame(static_cast<int>(remaining));
140
- if (!pkt) {
141
- continue;
142
- }
143
- if (pkt->type == PacketType::ACK) {
144
- return;
145
- }
146
- if (pkt->type == PacketType::NAK) {
147
- if (attempts > config_.retryCount) {
148
- throw std::runtime_error("ECR17: NAK after " + std::to_string(attempts) +
149
- " attempts");
150
- }
151
- std::this_thread::sleep_for(std::chrono::milliseconds(config_.retryDelayMs));
152
- transport_.send(requestFrame);
153
- ++attempts;
154
- deadline = clock::now() + std::chrono::milliseconds(config_.ackTimeoutMs);
155
- continue;
156
- }
157
- if (pkt->type == PacketType::APPLICATION) {
158
- // The terminal sent the application response without (or before) a
159
- // physical ACK. The request was clearly received, so treat the
160
- // handshake as satisfied and stash the frame for waitForResult() to
161
- // validate/ACK — dropping it would lose a completed transaction.
162
- pendingResult_ = pkt;
163
- return;
164
- }
165
- // Ignore any progress frames that may precede the ACK.
166
- }
167
- }
168
-
169
- DecodedPacket Ecr17Session::exchange(const std::string& requestPayload) {
170
- resetForNewTransaction(); // start clean (reusable across reconnects)
171
- ackHandshake(requestPayload); // send + physical ACK handshake (with retransmission)
172
- return waitForResult();
173
- }
174
-
175
- DecodedPacket Ecr17Session::exchangeWithAdditionalData(const std::string& requestPayload,
176
- const std::string& additionalPayload) {
177
- resetForNewTransaction();
178
- ackHandshake(requestPayload); // main request -> ACK
179
- ackHandshake(additionalPayload); // 'U' additional-data message -> ACK
180
- return waitForResult();
181
- }
182
-
183
- DecodedPacket Ecr17Session::waitForResult() {
184
- auto deadline = clock::now() + std::chrono::milliseconds(config_.responseTimeoutMs);
185
- while (true) {
186
- std::optional<DecodedPacket> pkt;
187
- if (pendingResult_) {
188
- // A frame the ACK handshake received early — process it first.
189
- pkt = std::move(pendingResult_);
190
- pendingResult_.reset();
191
- } else {
192
- const auto remaining =
193
- std::chrono::duration_cast<std::chrono::milliseconds>(deadline - clock::now())
194
- .count();
195
- if (remaining <= 0) {
196
- throw std::runtime_error("ECR17: no application response before timeout");
197
- }
198
- pkt = waitForFrame(static_cast<int>(remaining));
199
- if (!pkt) {
200
- continue;
201
- }
202
- }
203
- switch (pkt->type) {
204
- case PacketType::PROGRESS:
205
- if (onProgress_) onProgress_(pkt->payload);
206
- break;
207
- case PacketType::APPLICATION:
208
- if (!pkt->validLrc) {
209
- sendControl(PacketCodec::NAK);
210
- break;
211
- }
212
- sendControl(PacketCodec::ACK);
213
- if (isReceipt(pkt->payload)) {
214
- if (onReceiptLine_) onReceiptLine_(pkt->payload);
215
- break;
216
- }
217
- drainReceipts(); // forward receipts that follow the result (if enabled)
218
- return *pkt;
219
- case PacketType::ACK:
220
- case PacketType::NAK:
221
- break; // stray confirmation; ignore
222
- case PacketType::UNKNOWN:
223
- sendControl(PacketCodec::NAK);
224
- break;
225
- }
226
- }
227
- }
228
-
229
- void Ecr17Session::drainReceipts() {
230
- if (config_.receiptDrainMs <= 0) {
231
- return;
232
- }
233
- // Keep forwarding 'S' receipt lines that arrive after the result until the
234
- // terminal goes quiet for receiptDrainMs.
235
- while (true) {
236
- std::optional<DecodedPacket> pkt = waitForFrame(config_.receiptDrainMs);
237
- if (!pkt) {
238
- return; // idle: no more receipts
239
- }
240
- switch (pkt->type) {
241
- case PacketType::APPLICATION:
242
- if (pkt->validLrc) {
243
- sendControl(PacketCodec::ACK);
244
- if (isReceipt(pkt->payload) && onReceiptLine_) {
245
- onReceiptLine_(pkt->payload);
246
- }
247
- } else {
248
- sendControl(PacketCodec::NAK); // request retransmit, like waitForResult
249
- }
250
- break;
251
- case PacketType::PROGRESS:
252
- if (onProgress_) onProgress_(pkt->payload);
253
- break;
254
- default:
255
- break;
256
- }
257
- }
258
- }
259
-
260
- } // namespace margelo::nitro::ecr17
1
+ #include "Session/Ecr17Session.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <chrono>
5
+ #include <stdexcept>
6
+ #include <thread>
7
+
8
+ namespace margelo::nitro::ecr17 {
9
+
10
+ using clock = std::chrono::steady_clock;
11
+
12
+ Ecr17Session::Ecr17Session(Transport& transport, const SessionConfig& config)
13
+ : transport_(transport), config_(config), codec_(config.lrcMode) {
14
+ transport_.setDataCallback([this](const std::vector<uint8_t>& data) { onData(data); });
15
+ transport_.setDisconnectCallback([this]() { onDisconnect(); });
16
+ }
17
+
18
+ void Ecr17Session::onData(const std::vector<uint8_t>& data) {
19
+ {
20
+ std::lock_guard<std::mutex> lock(mutex_);
21
+ rxBuffer_.insert(rxBuffer_.end(), data.begin(), data.end());
22
+ }
23
+ cv_.notify_all();
24
+ }
25
+
26
+ void Ecr17Session::onDisconnect() {
27
+ {
28
+ std::lock_guard<std::mutex> lock(mutex_);
29
+ disconnected_ = true;
30
+ }
31
+ cv_.notify_all();
32
+ }
33
+
34
+ // Extracts one complete frame from the front of rxBuffer_, dropping leading junk
35
+ // bytes to resynchronise. Returns nullopt if no complete frame is available yet.
36
+ // Caller must hold mutex_.
37
+ std::optional<std::vector<uint8_t>> Ecr17Session::extractFrameLocked() {
38
+ while (!rxBuffer_.empty()) {
39
+ const uint8_t first = rxBuffer_.front();
40
+
41
+ if (first == PacketCodec::ACK || first == PacketCodec::NAK) {
42
+ if (rxBuffer_.size() < 3) {
43
+ return std::nullopt; // wait for ETX + LRC
44
+ }
45
+ std::vector<uint8_t> frame(rxBuffer_.begin(), rxBuffer_.begin() + 3);
46
+ rxBuffer_.erase(rxBuffer_.begin(), rxBuffer_.begin() + 3);
47
+ return frame;
48
+ }
49
+
50
+ if (first == PacketCodec::STX) {
51
+ auto etx = std::find(rxBuffer_.begin(), rxBuffer_.end(), PacketCodec::ETX);
52
+ if (etx == rxBuffer_.end() || etx + 1 == rxBuffer_.end()) {
53
+ return std::nullopt; // wait for ETX and the trailing LRC
54
+ }
55
+ auto lastByte = etx + 1; // LRC
56
+ std::vector<uint8_t> frame(rxBuffer_.begin(), lastByte + 1);
57
+ rxBuffer_.erase(rxBuffer_.begin(), lastByte + 1);
58
+ return frame;
59
+ }
60
+
61
+ if (first == PacketCodec::SOH) {
62
+ auto eot = std::find(rxBuffer_.begin(), rxBuffer_.end(), PacketCodec::EOT);
63
+ if (eot == rxBuffer_.end()) {
64
+ return std::nullopt; // wait for EOT
65
+ }
66
+ std::vector<uint8_t> frame(rxBuffer_.begin(), eot + 1);
67
+ rxBuffer_.erase(rxBuffer_.begin(), eot + 1);
68
+ return frame;
69
+ }
70
+
71
+ // Unrecognised lead byte: drop it and resynchronise.
72
+ rxBuffer_.erase(rxBuffer_.begin());
73
+ }
74
+ return std::nullopt;
75
+ }
76
+
77
+ std::optional<DecodedPacket> Ecr17Session::waitForFrame(int timeoutMs) {
78
+ std::unique_lock<std::mutex> lock(mutex_);
79
+ const auto deadline = clock::now() + std::chrono::milliseconds(timeoutMs);
80
+ while (true) {
81
+ if (auto frame = extractFrameLocked()) {
82
+ return codec_.decode(*frame);
83
+ }
84
+ if (disconnected_) {
85
+ throw std::runtime_error("ECR17: transport disconnected during exchange");
86
+ }
87
+ if (cv_.wait_until(lock, deadline) == std::cv_status::timeout) {
88
+ if (auto frame = extractFrameLocked()) {
89
+ return codec_.decode(*frame);
90
+ }
91
+ return std::nullopt;
92
+ }
93
+ }
94
+ }
95
+
96
+ void Ecr17Session::sendControl(uint8_t control) {
97
+ transport_.send(codec_.encodeControl(control));
98
+ }
99
+
100
+ bool Ecr17Session::isReceipt(const std::string& payload) {
101
+ // Send-ticket message from the terminal uses message code 'S' at position 10.
102
+ return payload.size() >= 10 && payload[9] == 'S';
103
+ }
104
+
105
+ void Ecr17Session::resetForNewTransaction() {
106
+ std::lock_guard<std::mutex> lock(mutex_);
107
+ disconnected_ = false;
108
+ rxBuffer_.clear();
109
+ pendingResult_.reset();
110
+ }
111
+
112
+ void Ecr17Session::sendAckOnly(const std::string& requestPayload) {
113
+ resetForNewTransaction();
114
+ ackHandshake(requestPayload);
115
+ }
116
+
117
+ void Ecr17Session::ackHandshake(const std::string& requestPayload) {
118
+ const std::vector<uint8_t> requestFrame = codec_.encodeApplication(requestPayload);
119
+
120
+ transport_.send(requestFrame);
121
+ int attempts = 1;
122
+ auto deadline = clock::now() + std::chrono::milliseconds(config_.ackTimeoutMs);
123
+
124
+ while (true) {
125
+ const auto remaining =
126
+ std::chrono::duration_cast<std::chrono::milliseconds>(deadline - clock::now()).count();
127
+ if (remaining <= 0) {
128
+ if (attempts > config_.retryCount) {
129
+ throw std::runtime_error("ECR17: no ACK after " + std::to_string(attempts) +
130
+ " attempts");
131
+ }
132
+ std::this_thread::sleep_for(std::chrono::milliseconds(config_.retryDelayMs));
133
+ transport_.send(requestFrame);
134
+ ++attempts;
135
+ deadline = clock::now() + std::chrono::milliseconds(config_.ackTimeoutMs);
136
+ continue;
137
+ }
138
+
139
+ std::optional<DecodedPacket> pkt = waitForFrame(static_cast<int>(remaining));
140
+ if (!pkt) {
141
+ continue;
142
+ }
143
+ if (pkt->type == PacketType::ACK) {
144
+ return;
145
+ }
146
+ if (pkt->type == PacketType::NAK) {
147
+ if (attempts > config_.retryCount) {
148
+ throw std::runtime_error("ECR17: NAK after " + std::to_string(attempts) +
149
+ " attempts");
150
+ }
151
+ std::this_thread::sleep_for(std::chrono::milliseconds(config_.retryDelayMs));
152
+ transport_.send(requestFrame);
153
+ ++attempts;
154
+ deadline = clock::now() + std::chrono::milliseconds(config_.ackTimeoutMs);
155
+ continue;
156
+ }
157
+ if (pkt->type == PacketType::APPLICATION) {
158
+ // The terminal sent the application response without (or before) a
159
+ // physical ACK. The request was clearly received, so treat the
160
+ // handshake as satisfied and stash the frame for waitForResult() to
161
+ // validate/ACK — dropping it would lose a completed transaction.
162
+ pendingResult_ = pkt;
163
+ return;
164
+ }
165
+ // Ignore any progress frames that may precede the ACK.
166
+ }
167
+ }
168
+
169
+ DecodedPacket Ecr17Session::exchange(const std::string& requestPayload) {
170
+ resetForNewTransaction(); // start clean (reusable across reconnects)
171
+ ackHandshake(requestPayload); // send + physical ACK handshake (with retransmission)
172
+ return waitForResult();
173
+ }
174
+
175
+ DecodedPacket Ecr17Session::exchangeWithAdditionalData(const std::string& requestPayload,
176
+ const std::string& additionalPayload) {
177
+ resetForNewTransaction();
178
+ ackHandshake(requestPayload); // main request -> ACK
179
+ ackHandshake(additionalPayload); // 'U' additional-data message -> ACK
180
+ return waitForResult();
181
+ }
182
+
183
+ DecodedPacket Ecr17Session::waitForResult() {
184
+ auto deadline = clock::now() + std::chrono::milliseconds(config_.responseTimeoutMs);
185
+ while (true) {
186
+ std::optional<DecodedPacket> pkt;
187
+ if (pendingResult_) {
188
+ // A frame the ACK handshake received early — process it first.
189
+ pkt = std::move(pendingResult_);
190
+ pendingResult_.reset();
191
+ } else {
192
+ const auto remaining =
193
+ std::chrono::duration_cast<std::chrono::milliseconds>(deadline - clock::now())
194
+ .count();
195
+ if (remaining <= 0) {
196
+ throw std::runtime_error("ECR17: no application response before timeout");
197
+ }
198
+ pkt = waitForFrame(static_cast<int>(remaining));
199
+ if (!pkt) {
200
+ continue;
201
+ }
202
+ }
203
+ switch (pkt->type) {
204
+ case PacketType::PROGRESS:
205
+ if (onProgress_) onProgress_(pkt->payload);
206
+ break;
207
+ case PacketType::APPLICATION:
208
+ if (!pkt->validLrc) {
209
+ sendControl(PacketCodec::NAK);
210
+ break;
211
+ }
212
+ sendControl(PacketCodec::ACK);
213
+ if (isReceipt(pkt->payload)) {
214
+ if (onReceiptLine_) onReceiptLine_(pkt->payload);
215
+ break;
216
+ }
217
+ drainReceipts(); // forward receipts that follow the result (if enabled)
218
+ return *pkt;
219
+ case PacketType::ACK:
220
+ case PacketType::NAK:
221
+ break; // stray confirmation; ignore
222
+ case PacketType::UNKNOWN:
223
+ sendControl(PacketCodec::NAK);
224
+ break;
225
+ }
226
+ }
227
+ }
228
+
229
+ void Ecr17Session::drainReceipts() {
230
+ if (config_.receiptDrainMs <= 0) {
231
+ return;
232
+ }
233
+ // Keep forwarding 'S' receipt lines that arrive after the result until the
234
+ // terminal goes quiet for receiptDrainMs.
235
+ while (true) {
236
+ std::optional<DecodedPacket> pkt = waitForFrame(config_.receiptDrainMs);
237
+ if (!pkt) {
238
+ return; // idle: no more receipts
239
+ }
240
+ switch (pkt->type) {
241
+ case PacketType::APPLICATION:
242
+ if (pkt->validLrc) {
243
+ sendControl(PacketCodec::ACK);
244
+ if (isReceipt(pkt->payload) && onReceiptLine_) {
245
+ onReceiptLine_(pkt->payload);
246
+ }
247
+ } else {
248
+ sendControl(PacketCodec::NAK); // request retransmit, like waitForResult
249
+ }
250
+ break;
251
+ case PacketType::PROGRESS:
252
+ if (onProgress_) onProgress_(pkt->payload);
253
+ break;
254
+ default:
255
+ break;
256
+ }
257
+ }
258
+ }
259
+
260
+ } // namespace margelo::nitro::ecr17