ยท 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
Back to Blog