ยท 5 min read
Jpos-glue
find . -type f | while read -r file; do
echo "===== $file ====="
cat "$file"
echo
done
===== ./comm-adapters/serial-bridge/pom.xml =====
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>pos-solution</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>serial-bridge</artifactId>
<dependencies>
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.10.4</version>
</dependency>
</dependencies>
</project>
===== ./comm-adapters/serial-bridge/src/main/java/com/example/bridge/SerialBridge.java =====
package com.example.bridge;
import com.fazecast.jSerialComm.SerialPort;
import java.io.IOException;
import java.time.Duration;
import java.util.function.Predicate;
public class SerialBridge {
private SerialPort port;
public SerialBridge(String portName) {
port = SerialPort.getCommPort(portName);
port.setBaudRate(9600);
port.openPort();
}
public synchronized void write(byte[] data) throws IOException {
port.getOutputStream().write(data);
port.getOutputStream().flush();
}
public void expectAck(Duration timeout) throws IOException {
long limit = System.currentTimeMillis() + timeout.toMillis();
while (System.currentTimeMillis() < limit) {
if (port.bytesAvailable() > 0) {
int b = port.getInputStream().read();
if (b == 0x06) return; // ACK
}
}
throw new IOException("ACK timeout");
}
public byte[] readResponse(Duration timeout, Predicate<byte[]> term) throws IOException {
long limit = System.currentTimeMillis() + timeout.toMillis();
java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();
while (System.currentTimeMillis() < limit) {
while (port.bytesAvailable() > 0) {
buf.write(port.getInputStream().read());
if (term.test(buf.toByteArray())) {
return buf.toByteArray();
}
}
}
throw new IOException("Resp timeout");
}
}
===== ./config/jpos.xml =====
<JposEntries>
<JposEntry logicalName="MYCASH">
<creation factoryClass="org.javapos.services.SimpleServiceFactory"
serviceClass="com.example.mybrand.MyCashChangerService"/>
<vendor name="MyBrand"/>
<deviceCategory>CashChanger</deviceCategory>
<product name="MyCashChanger"/>
<property name="PortName" value="COM3"/>
</JposEntry>
</JposEntries>
===== ./pom.xml =====
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>pos-solution</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>control-core</module>
<module>comm-adapters/serial-bridge</module>
<module>device-mybrand-cashchanger</module>
</modules>
</project>
===== ./control-core/pom.xml =====
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>pos-solution</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>control-core</artifactId>
<dependencies>
<!-- JavaPOS contracts (bring your own jar) -->
<dependency>
<groupId>org.javapos</groupId>
<artifactId>javapos-contracts</artifactId>
<version>1.14.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
===== ./control-core/src/main/java/com/example/core/DeviceStateMachine.java =====
package com.example.core;
import java.util.concurrent.atomic.AtomicReference;
public class DeviceStateMachine {
public enum State { CLOSED, CLAIMED, ENABLED, BUSY, IDLE, ERROR }
private final AtomicReference<State> state = new AtomicReference<>(State.CLOSED);
public State get() {
return state.get();
}
public void transitionTo(State target) {
State prev = state.getAndSet(target);
// sanity: CLOSED -> ENABLED not allowed etc. (simplified)
}
}
===== ./control-core/src/main/java/com/example/core/AbstractCashChangerControl.java =====
package com.example.core;
import jpos.JposException;
public abstract class AbstractCashChangerControl {
protected final AbstractCashChangerService service;
protected AbstractCashChangerControl(AbstractCashChangerService svc) {
this.service = svc;
}
public synchronized void dispenseCash(String combo) throws JposException {
service.runWithPolicy(Command.DISPENSE_CASH, combo);
}
public synchronized Object readCashCounts() throws JposException {
return service.runWithPolicy(Command.READ_CASH_COUNTS);
}
}
===== ./control-core/src/main/java/com/example/core/RetryPolicy.java =====
package com.example.core;
import java.time.Duration;
public record RetryPolicy(Duration timeout, int maxRetries, byte[] abortBytes) {
public static final RetryPolicy NONE = new RetryPolicy(Duration.ofSeconds(5), 0, new byte[0]);
}
===== ./control-core/src/main/java/com/example/core/Command.java =====
package com.example.core;
public enum Command {
DISPENSE_CASH,
READ_CASH_COUNTS,
SMART_DISPENSE,
DIRECT_IO
}
===== ./control-core/src/main/java/com/example/core/AbstractCashChangerService.java =====
package com.example.core;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.*;
import jpos.JposException;
import jpos.JposConst;
public abstract class AbstractCashChangerService {
protected final DeviceStateMachine sm = new DeviceStateMachine();
protected final ExecutorService executor = Executors.newSingleThreadExecutor();
protected Map<Command, RetryPolicy> policyMap = Map.of();
protected abstract Object doExecute(Command cmd, Object... args) throws JposException;
public Object runWithPolicy(Command cmd, Object... args) throws JposException {
RetryPolicy p = policyMap.getOrDefault(cmd, RetryPolicy.NONE);
for (int i = 0; i <= p.maxRetries(); i++) {
try {
Future<Object> fut = executor.submit(() -> doExecute(cmd, args));
return fut.get(p.timeout().toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException te) {
if (i == p.maxRetries()) {
// would send abort here
throw new JposException(JposConst.JPOS_E_TIMEOUT, "Timeout & aborted");
}
} catch (Exception e) {
if (e instanceof JposException je) throw je;
throw new JposException(JposConst.JPOS_E_FAILURE, e.getMessage());
}
}
return null;
}
}
===== ./device-mybrand-cashchanger/pom.xml =====
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>pos-solution</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>device-mybrand-cashchanger</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>control-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>serial-bridge</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
===== ./device-mybrand-cashchanger/src/main/resources/META-INF/services/jpos.services.CashChangerService113 =====
com.example.mybrand.MyCashChangerService
===== ./device-mybrand-cashchanger/src/main/resources/MyRetryPolicy.yml =====
DISPENSE_CASH:
timeout: 2s
maxRetries: 1
abortHex: "18"
SMART_DISPENSE:
timeout: 5s
maxRetries: 1
abortHex: "18"
===== ./device-mybrand-cashchanger/src/main/java/com/example/mybrand/MyCashChangerService.java =====
package com.example.mybrand;
import com.example.core.*;
import com.example.bridge.SerialBridge;
import jpos.JposException;
import jpos.JposConst;
import java.time.Duration;
public class MyCashChangerService extends AbstractCashChangerService {
private final MyCashChangerCodec codec = new MyCashChangerCodec();
private final SerialBridge bridge = new MyCashChangerBridge("COM3");
public MyCashChangerService() {
policyMap = java.util.Map.of(
Command.DISPENSE_CASH, new RetryPolicy(Duration.ofSeconds(2),1,codec.abort()),
Command.SMART_DISPENSE, new RetryPolicy(Duration.ofSeconds(5),1,codec.abort())
);
}
@Override
protected Object doExecute(Command cmd, Object... args) throws JposException {
try {
switch (cmd) {
case DISPENSE_CASH -> {
String combo = (String) args[0];
bridge.write(codec.encodeDispense(combo));
bridge.expectAck(Duration.ofSeconds(2));
return null;
}
case READ_CASH_COUNTS -> {
bridge.write("CNT?\r\n".getBytes());
bridge.expectAck(Duration.ofSeconds(1));
byte[] resp = bridge.readResponse(Duration.ofSeconds(2), codec.respEndForCounts());
return new String(resp); // placeholder
}
case SMART_DISPENSE -> {
int amount = (Integer) args[0];
String combo = "1000,1"; // placeholder calc
bridge.write(codec.encodeDispense(combo));
bridge.expectAck(Duration.ofSeconds(2));
byte[] res = bridge.readResponse(Duration.ofSeconds(3), codec::isSmartDispenseEnd);
// In real impl, parse and fire event
return null;
}
default -> throw new JposException(JposConst.JPOS_E_ILLEGAL, "Unsupported cmd");
}
} catch (Exception e) {
if (e instanceof JposException je) throw je;
throw new JposException(JposConst.JPOS_E_FAILURE, e.getMessage());
}
}
}
===== ./device-mybrand-cashchanger/src/main/java/com/example/mybrand/MyCashChangerControlEx.java =====
package com.example.mybrand;
import jpos.JposException;
public interface MyCashChangerControlEx {
void smartDispense(int amountYen) throws JposException;
}
===== ./device-mybrand-cashchanger/src/main/java/com/example/mybrand/MyCashChangerCodec.java =====
package com.example.mybrand;
import java.util.function.Predicate;
public class MyCashChangerCodec {
public byte[] encodeDispense(String combo) {
return (combo + "\r\n").getBytes();
}
public Predicate<byte[]> respEndForCounts() {
return bytes -> bytes.length >= 24;
}
public boolean isSmartDispenseEnd(byte[] bytes) {
return bytes.length > 0 && bytes[bytes.length-1] == '\n';
}
public byte[] abort() {
return new byte[]{0x18}; // CAN
}
}
===== ./device-mybrand-cashchanger/src/main/java/com/example/mybrand/MyCashChangerControl.java =====
package com.example.mybrand;
import com.example.core.*;
import jpos.JposException;
public class MyCashChangerControl extends AbstractCashChangerControl implements MyCashChangerControlEx {
public MyCashChangerControl() {
super(new MyCashChangerService());
}
@Override
public void smartDispense(int amountYen) throws JposException {
service.runWithPolicy(Command.SMART_DISPENSE, amountYen);
}
}
===== ./device-mybrand-cashchanger/src/main/java/com/example/mybrand/MyCashChangerBridge.java =====
package com.example.mybrand;
import com.example.bridge.SerialBridge;
public class MyCashChangerBridge extends SerialBridge {
public MyCashChangerBridge(String port) {
super(port);
}
}
tree .
.
โโโ comm-adapters
โย ย โโโ serial-bridge
โย ย โโโ pom.xml
โย ย โโโ src
โย ย โโโ main
โย ย โโโ java
โย ย โโโ com
โย ย โโโ example
โย ย โโโ bridge
โย ย โโโ SerialBridge.java
โโโ config
โย ย โโโ jpos.xml
โโโ control-core
โย ย โโโ pom.xml
โย ย โโโ src
โย ย โโโ main
โย ย โโโ java
โย ย โโโ com
โย ย โโโ example
โย ย โโโ core
โย ย โโโ AbstractCashChangerControl.java
โย ย โโโ AbstractCashChangerService.java
โย ย โโโ Command.java
โย ย โโโ DeviceStateMachine.java
โย ย โโโ RetryPolicy.java
โโโ device-mybrand-cashchanger
โย ย โโโ pom.xml
โย ย โโโ src
โย ย โโโ main
โย ย โโโ java
โย ย โย ย โโโ com
โย ย โย ย โโโ example
โย ย โย ย โโโ mybrand
โย ย โย ย โโโ MyCashChangerBridge.java
โย ย โย ย โโโ MyCashChangerCodec.java
โย ย โย ย โโโ MyCashChangerControl.java
โย ย โย ย โโโ MyCashChangerControlEx.java
โย ย โย ย โโโ MyCashChangerService.java
โย ย โโโ resources
โย ย โโโ META-INF
โย ย โย ย โโโ services
โย ย โย ย โโโ jpos.services.CashChangerService113
โย ย โโโ MyRetryPolicy.yml
โโโ pom.xml
27 directories, 18 files
Share: