@padosoft/react-native-ecr17 0.0.0 → 2.0.0
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.
- package/Ecr17.podspec +39 -39
- package/README.md +348 -348
- package/android/CMakeLists.txt +41 -41
- package/android/build.gradle +148 -148
- package/android/fix-prefab.gradle +50 -50
- package/android/gradle.properties +5 -5
- package/android/src/main/AndroidManifest.xml +2 -2
- package/android/src/main/cpp/cpp-adapter.cpp +8 -8
- package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -233
- package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -30
- package/cpp/Ecr17.hpp +1 -1
- package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -598
- package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -85
- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -277
- package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -103
- package/cpp/Ecr17Response/Ecr17Response.cpp +155 -155
- package/cpp/Ecr17Response/Ecr17Response.hpp +113 -113
- package/cpp/Lcr/Lcr.cpp +42 -42
- package/cpp/Lcr/Lcr.hpp +21 -21
- package/cpp/PacketCodec/PacketCodec.cpp +145 -145
- package/cpp/PacketCodec/PacketCodec.hpp +47 -47
- package/cpp/Session/Ecr17Session.cpp +260 -260
- package/cpp/Session/Ecr17Session.hpp +97 -97
- package/cpp/Session/RetryPolicy.hpp +23 -23
- package/cpp/Transport/FakeTransport.hpp +95 -95
- package/cpp/Transport/NativeTransportAdapter.cpp +42 -42
- package/cpp/Transport/NativeTransportAdapter.hpp +32 -32
- package/cpp/Transport/Transport.hpp +30 -30
- package/cpp/tests/CMakeLists.txt +55 -55
- package/cpp/tests/PosixTcpTransport.hpp +105 -105
- package/cpp/tests/stubs/LrcMode.hpp +25 -25
- package/cpp/tests/test_flows.cpp +148 -148
- package/cpp/tests/test_integration_terminal.cpp +72 -72
- package/cpp/tests/test_lrc.cpp +66 -66
- package/cpp/tests/test_packet_codec.cpp +164 -164
- package/cpp/tests/test_protocol.cpp +102 -102
- package/cpp/tests/test_protocol_commands.cpp +190 -190
- package/cpp/tests/test_response.cpp +164 -164
- package/cpp/tests/test_retry_policy.cpp +28 -28
- package/cpp/tests/test_session.cpp +262 -262
- package/ios/HybridEcr17Transport.swift +103 -103
- package/lib/commonjs/index.js +50 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/client.nitro.js +17 -0
- package/lib/commonjs/specs/client.nitro.js.map +1 -0
- package/lib/commonjs/specs/transport.nitro.js +6 -0
- package/lib/commonjs/specs/transport.nitro.js.map +1 -0
- package/lib/commonjs/types/client.js +2 -0
- package/lib/commonjs/types/client.js.map +1 -0
- package/lib/commonjs/utils/client.js +13 -0
- package/lib/commonjs/utils/client.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/specs/client.nitro.js +13 -0
- package/lib/module/specs/client.nitro.js.map +1 -0
- package/lib/module/specs/transport.nitro.js +4 -0
- package/lib/module/specs/transport.nitro.js.map +1 -0
- package/lib/module/types/client.js +2 -0
- package/lib/module/types/client.js.map +1 -0
- package/lib/module/utils/client.js +9 -0
- package/lib/module/utils/client.js.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/specs/client.nitro.d.ts +63 -0
- package/lib/typescript/src/specs/client.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/transport.nitro.d.ts +13 -0
- package/lib/typescript/src/specs/transport.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types/client.d.ts +138 -0
- package/lib/typescript/src/types/client.d.ts.map +1 -0
- package/lib/typescript/src/utils/client.d.ts +3 -0
- package/lib/typescript/src/utils/client.d.ts.map +1 -0
- package/nitro.json +30 -30
- package/package.json +4 -4
- package/react-native.config.js +18 -18
- package/src/index.ts +4 -4
- package/src/specs/client.nitro.ts +102 -102
- package/src/specs/transport.nitro.ts +25 -25
- package/src/types/client.ts +196 -196
- 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
|