@padosoft/react-native-ecr17 0.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 -0
- package/README.md +348 -0
- package/android/CMakeLists.txt +41 -0
- package/android/build.gradle +149 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -0
- package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -0
- package/cpp/Ecr17.cpp +1 -0
- package/cpp/Ecr17.hpp +2 -0
- package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -0
- package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -0
- package/cpp/Ecr17Response/Ecr17Response.cpp +155 -0
- package/cpp/Ecr17Response/Ecr17Response.hpp +113 -0
- package/cpp/Lcr/Lcr.cpp +42 -0
- package/cpp/Lcr/Lcr.hpp +22 -0
- package/cpp/PacketCodec/PacketCodec.cpp +146 -0
- package/cpp/PacketCodec/PacketCodec.hpp +48 -0
- package/cpp/Session/Ecr17Session.cpp +260 -0
- package/cpp/Session/Ecr17Session.hpp +97 -0
- package/cpp/Session/RetryPolicy.hpp +23 -0
- package/cpp/Transport/FakeTransport.hpp +95 -0
- package/cpp/Transport/NativeTransportAdapter.cpp +42 -0
- package/cpp/Transport/NativeTransportAdapter.hpp +32 -0
- package/cpp/Transport/Transport.hpp +31 -0
- package/cpp/tests/CMakeLists.txt +55 -0
- package/cpp/tests/PosixTcpTransport.hpp +105 -0
- package/cpp/tests/stubs/LrcMode.hpp +25 -0
- package/cpp/tests/test_flows.cpp +148 -0
- package/cpp/tests/test_integration_terminal.cpp +72 -0
- package/cpp/tests/test_lrc.cpp +66 -0
- package/cpp/tests/test_packet_codec.cpp +164 -0
- package/cpp/tests/test_protocol.cpp +102 -0
- package/cpp/tests/test_protocol_commands.cpp +190 -0
- package/cpp/tests/test_response.cpp +164 -0
- package/cpp/tests/test_retry_policy.cpp +28 -0
- package/cpp/tests/test_session.cpp +262 -0
- package/ios/Bridge.h +1 -0
- package/ios/HybridEcr17Transport.swift +103 -0
- package/nitro.json +30 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/Ecr17+autolinking.cmake +82 -0
- package/nitrogen/generated/android/Ecr17+autolinking.gradle +27 -0
- package/nitrogen/generated/android/Ecr17OnLoad.cpp +68 -0
- package/nitrogen/generated/android/Ecr17OnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.cpp +93 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.hpp +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Ecr17OnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void_std__shared_ptr_ArrayBuffer_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/HybridEcr17TransportSpec.kt +86 -0
- package/nitrogen/generated/ios/Ecr17+autolinking.rb +62 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.hpp +154 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Umbrella.hpp +47 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.mm +43 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.hpp +119 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec_cxx.swift +211 -0
- package/nitrogen/generated/shared/c++/CardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/CardVerificationRequest.hpp +97 -0
- package/nitrogen/generated/shared/c++/CardVerificationResult.hpp +136 -0
- package/nitrogen/generated/shared/c++/CloseSessionResult.hpp +106 -0
- package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
- package/nitrogen/generated/shared/c++/CurrencyExchange.hpp +100 -0
- package/nitrogen/generated/shared/c++/Ecr17Config.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.cpp +42 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/IncrementalAuthRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/LrcMode.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentCardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PaymentResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/PosStatusResponse.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthClosureRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PreAuthResult.hpp +144 -0
- package/nitrogen/generated/shared/c++/ProgressEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReceiptLine.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReversalRequest.hpp +88 -0
- package/nitrogen/generated/shared/c++/ReversalResult.hpp +132 -0
- package/nitrogen/generated/shared/c++/TokenizationRequest.hpp +89 -0
- package/nitrogen/generated/shared/c++/TokenizationService.hpp +76 -0
- package/nitrogen/generated/shared/c++/TotalsResult.hpp +93 -0
- package/nitrogen/generated/shared/c++/TransactionEntryMode.hpp +92 -0
- package/nitrogen/generated/shared/c++/TransactionOutcome.hpp +88 -0
- package/nitrogen/generated/shared/c++/VasResult.hpp +96 -0
- package/package.json +102 -0
- package/react-native.config.js +18 -0
- package/src/index.ts +4 -0
- package/src/specs/client.nitro.ts +102 -0
- package/src/specs/transport.nitro.ts +25 -0
- package/src/types/client.ts +196 -0
- package/src/utils/client.ts +10 -0
package/cpp/Lcr/Lcr.cpp
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#include "Lcr.hpp"
|
|
2
|
+
|
|
3
|
+
namespace margelo::nitro::ecr17 {
|
|
4
|
+
|
|
5
|
+
static constexpr uint8_t STX = 0x02;
|
|
6
|
+
static constexpr uint8_t ETX = 0x03;
|
|
7
|
+
|
|
8
|
+
uint8_t Lrc::compute(const std::vector<uint8_t>& payload, LrcMode mode) {
|
|
9
|
+
uint8_t lrc = BASE;
|
|
10
|
+
|
|
11
|
+
switch (mode) {
|
|
12
|
+
case LrcMode::STX:
|
|
13
|
+
case LrcMode::STX_NOEXT:
|
|
14
|
+
lrc ^= STX;
|
|
15
|
+
break;
|
|
16
|
+
|
|
17
|
+
default:
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (auto b : payload) {
|
|
22
|
+
lrc ^= b;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (mode) {
|
|
26
|
+
case LrcMode::STX:
|
|
27
|
+
case LrcMode::NOEXT:
|
|
28
|
+
lrc ^= ETX;
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
default:
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return lrc;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
uint8_t Lrc::compute(const std::string& payload, LrcMode mode) {
|
|
39
|
+
return compute(std::vector<uint8_t>(payload.begin(), payload.end()), mode);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
} // namespace margelo::nitro::ecr17
|
package/cpp/Lcr/Lcr.hpp
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// cpp/core/Lrc.hpp
|
|
2
|
+
|
|
3
|
+
#pragma once
|
|
4
|
+
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <vector>
|
|
8
|
+
|
|
9
|
+
#include "LrcMode.hpp"
|
|
10
|
+
|
|
11
|
+
namespace margelo::nitro::ecr17 {
|
|
12
|
+
|
|
13
|
+
class Lrc {
|
|
14
|
+
public:
|
|
15
|
+
static constexpr uint8_t BASE = 0x7F;
|
|
16
|
+
|
|
17
|
+
static uint8_t compute(const std::vector<uint8_t>& payload, LrcMode mode);
|
|
18
|
+
|
|
19
|
+
static uint8_t compute(const std::string& payload, LrcMode mode);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
} // namespace margelo::nitro::ecr17
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#include "PacketCodec.hpp"
|
|
2
|
+
|
|
3
|
+
#include <algorithm> // std::find
|
|
4
|
+
#include <iterator> // std::distance
|
|
5
|
+
|
|
6
|
+
namespace margelo::nitro::ecr17 {
|
|
7
|
+
|
|
8
|
+
PacketCodec::PacketCodec(LrcMode mode) : lrcMode_(mode) {}
|
|
9
|
+
|
|
10
|
+
std::vector<uint8_t> PacketCodec::encodeApplication(const std::string& payload) {
|
|
11
|
+
std::vector<uint8_t> frame;
|
|
12
|
+
|
|
13
|
+
frame.push_back(STX);
|
|
14
|
+
|
|
15
|
+
frame.insert(frame.end(), payload.begin(), payload.end());
|
|
16
|
+
|
|
17
|
+
frame.push_back(ETX);
|
|
18
|
+
|
|
19
|
+
uint8_t lrc = Lrc::compute(payload, lrcMode_);
|
|
20
|
+
|
|
21
|
+
frame.push_back(lrc);
|
|
22
|
+
|
|
23
|
+
return frame;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
std::vector<uint8_t> PacketCodec::encodeControl(uint8_t ctrl) {
|
|
27
|
+
std::vector<uint8_t> frame;
|
|
28
|
+
|
|
29
|
+
frame.push_back(ctrl);
|
|
30
|
+
frame.push_back(ETX);
|
|
31
|
+
|
|
32
|
+
uint8_t lrc = Lrc::compute(std::vector<uint8_t>{ctrl}, lrcMode_);
|
|
33
|
+
|
|
34
|
+
frame.push_back(lrc);
|
|
35
|
+
|
|
36
|
+
return frame;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
DecodedPacket PacketCodec::decode(const std::vector<uint8_t>& data) {
|
|
40
|
+
if (data.empty()) {
|
|
41
|
+
return {
|
|
42
|
+
PacketType::UNKNOWN,
|
|
43
|
+
"",
|
|
44
|
+
false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
uint8_t first = data[0];
|
|
49
|
+
|
|
50
|
+
if (first == ACK) {
|
|
51
|
+
return {
|
|
52
|
+
PacketType::ACK,
|
|
53
|
+
"",
|
|
54
|
+
true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (first == NAK) {
|
|
59
|
+
return {
|
|
60
|
+
PacketType::NAK,
|
|
61
|
+
"",
|
|
62
|
+
true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (first == SOH) {
|
|
67
|
+
// Progress update packet: SOH + message + EOT (no LRC). We need at least
|
|
68
|
+
// SOH and the trailing EOT before stripping the first/last byte,
|
|
69
|
+
// otherwise the iterator range below would be invalid (last < first).
|
|
70
|
+
if (data.size() < 2) {
|
|
71
|
+
return {
|
|
72
|
+
PacketType::UNKNOWN,
|
|
73
|
+
"",
|
|
74
|
+
false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The spec mandates SOH + 20-char message + EOT. Reject any frame whose
|
|
79
|
+
// final byte is not EOT so that garbage or a truncated read is never
|
|
80
|
+
// silently accepted as a valid progress update.
|
|
81
|
+
if (data.back() != EOT) {
|
|
82
|
+
return {
|
|
83
|
+
PacketType::UNKNOWN,
|
|
84
|
+
"",
|
|
85
|
+
false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
std::string payload(data.begin() + 1, data.end() - 1);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
PacketType::PROGRESS,
|
|
93
|
+
payload,
|
|
94
|
+
true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (first == STX) {
|
|
99
|
+
auto etxIt = std::find(data.begin(), data.end(), ETX);
|
|
100
|
+
|
|
101
|
+
if (etxIt == data.end()) {
|
|
102
|
+
return {
|
|
103
|
+
PacketType::UNKNOWN,
|
|
104
|
+
"",
|
|
105
|
+
false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
size_t etxIndex = std::distance(data.begin(), etxIt);
|
|
110
|
+
|
|
111
|
+
// A well-formed application frame is exactly STX + payload + ETX + LRC,
|
|
112
|
+
// so the LRC must be the final byte. Reject both a truncated frame (no
|
|
113
|
+
// LRC after ETX) and a buffer with trailing bytes -- e.g. a coalesced
|
|
114
|
+
// socket read holding a second frame ("STX..ETX LRC STX..") or garbage
|
|
115
|
+
// after the LRC. Splitting a byte stream into individual frames is the
|
|
116
|
+
// transport layer's responsibility, and DecodedPacket cannot carry the
|
|
117
|
+
// unconsumed remainder.
|
|
118
|
+
if (etxIndex + 2 != data.size()) {
|
|
119
|
+
return {
|
|
120
|
+
PacketType::UNKNOWN,
|
|
121
|
+
"",
|
|
122
|
+
false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
std::string payload(data.begin() + 1, data.begin() + etxIndex);
|
|
127
|
+
|
|
128
|
+
uint8_t rxLrc = data[etxIndex + 1];
|
|
129
|
+
|
|
130
|
+
uint8_t calcLrc = Lrc::compute(payload, lrcMode_);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
PacketType::APPLICATION,
|
|
134
|
+
payload,
|
|
135
|
+
rxLrc == calcLrc,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
PacketType::UNKNOWN,
|
|
141
|
+
"",
|
|
142
|
+
false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
} // namespace margelo::nitro::ecr17
|
|
@@ -0,0 +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
|
+
|
|
48
|
+
} // namespace margelo::nitro::ecr17
|
|
@@ -0,0 +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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <condition_variable>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <functional>
|
|
6
|
+
#include <mutex>
|
|
7
|
+
#include <optional>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <vector>
|
|
10
|
+
|
|
11
|
+
#include "Lcr/Lcr.hpp" // brings LrcMode
|
|
12
|
+
#include "PacketCodec/PacketCodec.hpp"
|
|
13
|
+
#include "Transport/Transport.hpp"
|
|
14
|
+
|
|
15
|
+
namespace margelo::nitro::ecr17 {
|
|
16
|
+
|
|
17
|
+
struct SessionConfig {
|
|
18
|
+
LrcMode lrcMode = LrcMode::STD;
|
|
19
|
+
int ackTimeoutMs = 2000; // wait for the physical ACK/NAK
|
|
20
|
+
int responseTimeoutMs = 60000; // wait for the application response
|
|
21
|
+
int retryCount = 3; // retransmissions on NAK/timeout (spec: up to 3)
|
|
22
|
+
int retryDelayMs = 200; // delay between retransmissions
|
|
23
|
+
int receiptDrainMs = 0; // after the result, keep forwarding 'S' receipt
|
|
24
|
+
// lines until this many ms of silence (0 = off)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Drives one ECR17 request/response exchange over a Transport: frames the
|
|
28
|
+
// request, handles the physical ACK/NAK handshake with retransmission, waits for
|
|
29
|
+
// the application response while forwarding progress (SOH) and receipt ('S')
|
|
30
|
+
// messages, and ACK/NAKs incoming frames per their LRC validity.
|
|
31
|
+
//
|
|
32
|
+
// Pure C++ and transport-agnostic: unit-tested against FakeTransport. A single
|
|
33
|
+
// exchange runs at a time (the protocol is one transaction per terminal).
|
|
34
|
+
class Ecr17Session {
|
|
35
|
+
public:
|
|
36
|
+
Ecr17Session(Transport& transport, const SessionConfig& config);
|
|
37
|
+
|
|
38
|
+
void setOnProgress(std::function<void(const std::string&)> cb) { onProgress_ = std::move(cb); }
|
|
39
|
+
void setOnReceiptLine(std::function<void(const std::string&)> cb) {
|
|
40
|
+
onReceiptLine_ = std::move(cb);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Sends `requestPayload` (the application message, without STX/ETX) and
|
|
44
|
+
// returns the decoded application result. Throws std::runtime_error on
|
|
45
|
+
// retransmission exhaustion, ACK/response timeout, or transport disconnect.
|
|
46
|
+
DecodedPacket exchange(const std::string& requestPayload);
|
|
47
|
+
|
|
48
|
+
// Like exchange(), but sends an extra additional-data message (command 'U',
|
|
49
|
+
// tokenization) after the main request is ACKed, before the result: the
|
|
50
|
+
// documented flow is request(flag=1) -> ACK -> 'U' -> ACK -> result.
|
|
51
|
+
DecodedPacket exchangeWithAdditionalData(const std::string& requestPayload,
|
|
52
|
+
const std::string& additionalPayload);
|
|
53
|
+
|
|
54
|
+
// For commands whose only reply is the physical ACK (e.g. enable/disable ECR
|
|
55
|
+
// printing 'E'). Performs the ACK handshake with retransmission and returns
|
|
56
|
+
// once ACK is received; does NOT wait for an application response. Throws on
|
|
57
|
+
// retransmission exhaustion / timeout / disconnect.
|
|
58
|
+
void sendAckOnly(const std::string& requestPayload);
|
|
59
|
+
|
|
60
|
+
private:
|
|
61
|
+
void onData(const std::vector<uint8_t>& data);
|
|
62
|
+
void onDisconnect();
|
|
63
|
+
// Clears stale RX bytes and the disconnected flag so the session is reusable
|
|
64
|
+
// across reconnects (a new transaction starts from a clean state).
|
|
65
|
+
void resetForNewTransaction();
|
|
66
|
+
// Sends a request and completes the physical ACK handshake (with
|
|
67
|
+
// retransmission). Does NOT reset state — callers reset once per transaction.
|
|
68
|
+
void ackHandshake(const std::string& requestPayload);
|
|
69
|
+
// Waits for the application result after the ACK handshake, forwarding
|
|
70
|
+
// progress (SOH) and receipt ('S') frames, NAKing invalid-LRC frames.
|
|
71
|
+
DecodedPacket waitForResult();
|
|
72
|
+
void drainReceipts();
|
|
73
|
+
std::optional<std::vector<uint8_t>> extractFrameLocked();
|
|
74
|
+
std::optional<DecodedPacket> waitForFrame(int timeoutMs);
|
|
75
|
+
void sendControl(uint8_t control);
|
|
76
|
+
static bool isReceipt(const std::string& payload);
|
|
77
|
+
|
|
78
|
+
Transport& transport_;
|
|
79
|
+
SessionConfig config_;
|
|
80
|
+
PacketCodec codec_;
|
|
81
|
+
|
|
82
|
+
std::mutex mutex_;
|
|
83
|
+
std::condition_variable cv_;
|
|
84
|
+
std::vector<uint8_t> rxBuffer_;
|
|
85
|
+
bool disconnected_ = false;
|
|
86
|
+
|
|
87
|
+
// Holds an application response that arrived during the ACK handshake (some
|
|
88
|
+
// terminals send the result before/without a physical ACK). Consumed by
|
|
89
|
+
// waitForResult() so the completed transaction's result is never dropped.
|
|
90
|
+
// Touched only by the worker thread, never the data-callback thread.
|
|
91
|
+
std::optional<DecodedPacket> pendingResult_{};
|
|
92
|
+
|
|
93
|
+
std::function<void(const std::string&)> onProgress_{};
|
|
94
|
+
std::function<void(const std::string&)> onReceiptLine_{};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
} // namespace margelo::nitro::ecr17
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
namespace margelo::nitro::ecr17 {
|
|
4
|
+
|
|
5
|
+
// Decides whether a command may be safely RE-SENT after an auto-reconnect.
|
|
6
|
+
//
|
|
7
|
+
// ⚠️ MONEY-CRITICAL INVARIANT: a financial command (safeToRetry == false) must
|
|
8
|
+
// NEVER be retried. If the connection drops after the terminal has processed the
|
|
9
|
+
// payment but before the response arrives, a blind re-send would charge the
|
|
10
|
+
// cardholder twice. Such cases are recovered by querying the terminal's last
|
|
11
|
+
// result (command 'G' / sendLastResult), NOT by retransmitting the request.
|
|
12
|
+
//
|
|
13
|
+
// Only read-only / idempotent commands (status, totals, sendLastResult,
|
|
14
|
+
// enable-printing) pass safeToRetry == true.
|
|
15
|
+
//
|
|
16
|
+
// Reconnecting the socket is a separate, always-safe action; this function only
|
|
17
|
+
// governs whether the *request* is replayed.
|
|
18
|
+
inline bool shouldRetryAfterReconnect(bool autoReconnect, bool transportDropped,
|
|
19
|
+
bool safeToRetry) {
|
|
20
|
+
return autoReconnect && transportDropped && safeToRetry;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
} // namespace margelo::nitro::ecr17
|