/*
* Copyright (C) 2014-2015 ULYSSIS VZW
*
* This file is part of i++.
*
* i++ is free software: you can redistribute it and/or modify
* it under the terms of version 3 of the GNU Affero General Public License
* as published by the Free Software Foundation. No other versions apply.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package org.ulyssis.ipp.reader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.llrp.ltk.exceptions.InvalidLLRPMessageException;
import org.llrp.ltk.generated.enumerations.AISpecStopTriggerType;
import org.llrp.ltk.generated.enumerations.AirProtocols;
import org.llrp.ltk.generated.enumerations.GetReaderCapabilitiesRequestedData;
import org.llrp.ltk.generated.enumerations.ROReportTriggerType;
import org.llrp.ltk.generated.enumerations.ROSpecStartTriggerType;
import org.llrp.ltk.generated.enumerations.ROSpecState;
import org.llrp.ltk.generated.enumerations.ROSpecStopTriggerType;
import org.llrp.ltk.generated.enumerations.StatusCode;
import org.llrp.ltk.generated.interfaces.EPCParameter;
import org.llrp.ltk.generated.messages.ADD_ROSPEC;
import org.llrp.ltk.generated.messages.ADD_ROSPEC_RESPONSE;
import org.llrp.ltk.generated.messages.DELETE_ROSPEC;
import org.llrp.ltk.generated.messages.DELETE_ROSPEC_RESPONSE;
import org.llrp.ltk.generated.messages.ENABLE_ROSPEC;
import org.llrp.ltk.generated.messages.ENABLE_ROSPEC_RESPONSE;
import org.llrp.ltk.generated.messages.GET_READER_CAPABILITIES;
import org.llrp.ltk.generated.messages.GET_READER_CAPABILITIES_RESPONSE;
import org.llrp.ltk.generated.messages.START_ROSPEC;
import org.llrp.ltk.generated.messages.START_ROSPEC_RESPONSE;
import org.llrp.ltk.generated.parameters.AISpec;
import org.llrp.ltk.generated.parameters.AISpecStopTrigger;
import org.llrp.ltk.generated.parameters.AntennaConfiguration;
import org.llrp.ltk.generated.parameters.InventoryParameterSpec;
import org.llrp.ltk.generated.parameters.RFTransmitter;
import org.llrp.ltk.generated.parameters.ROBoundarySpec;
import org.llrp.ltk.generated.parameters.ROReportSpec;
import org.llrp.ltk.generated.parameters.ROSpec;
import org.llrp.ltk.generated.parameters.ROSpecStartTrigger;
import org.llrp.ltk.generated.parameters.ROSpecStopTrigger;
import org.llrp.ltk.generated.parameters.TagReportContentSelector;
import org.llrp.ltk.generated.parameters.TransmitPowerLevelTableEntry;
import org.llrp.ltk.net.LLRPConnectionAttemptFailedException;
import org.llrp.ltk.net.LLRPConnector;
import org.llrp.ltk.net.LLRPEndpoint;
import org.llrp.ltk.types.Bit;
import org.llrp.ltk.types.LLRPMessage;
import org.llrp.ltk.types.UnsignedByte;
import org.llrp.ltk.types.UnsignedInteger;
import org.llrp.ltk.types.UnsignedShort;
import org.llrp.ltk.types.UnsignedShortArray;
import org.ulyssis.ipp.TagId;
import java.net.URI;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
/**
* This class is the result of information found on
* http://learn.impinj.com/articles/en_US/RFID/Reading-and-Writing-User-Memory-with-the-Java-LTK/
*/
public final class LLRPReader implements LLRPEndpoint {
private static final Logger LOG = LogManager.getLogger(LLRPReader.class);
private LLRPConnector reader;
private static final int TIMEOUT_MS = 10000;
private static final long CONNECT_TIMEOUT = 10000L;
private static final int ROSPEC_ID = 123;
private final Consumer<LLRPMessage> messageConsumer;
private final Consumer<String> errorConsumer;
/**
* Create a new reader, relay LLRP messages to
* the given messageConsumer, and errors to the
* given errorConsumer.
*/
public LLRPReader(Consumer<LLRPMessage> messageConsumer,
Consumer<String> errorConsumer) {
this.messageConsumer = messageConsumer;
this.errorConsumer = errorConsumer;
}
// Build the ROSpec.
// An ROSpec specifies start and stop triggers,
// tag report fields, antennas, etc.
public ROSpec buildROSpec() {
// Create a Reader Operation Spec (ROSpec).
ROSpec roSpec = new ROSpec();
roSpec.setPriority(new UnsignedByte(0));
roSpec.setCurrentState(new ROSpecState(ROSpecState.Disabled));
roSpec.setROSpecID(new UnsignedInteger(ROSPEC_ID));
// Set up the ROBoundarySpec
// This defines the start and stop triggers.
ROBoundarySpec roBoundarySpec = new ROBoundarySpec();
// Set the start trigger to null.
// This means the ROSpec will start as soon as it is enabled.
ROSpecStartTrigger startTrig = new ROSpecStartTrigger();
startTrig
.setROSpecStartTriggerType(new ROSpecStartTriggerType(ROSpecStartTriggerType.Null));
roBoundarySpec.setROSpecStartTrigger(startTrig);
// Set the stop trigger is null. This means the ROSpec
// will keep running until an STOP_ROSPEC message is sent.
ROSpecStopTrigger stopTrig = new ROSpecStopTrigger();
stopTrig.setDurationTriggerValue(new UnsignedInteger(0));
stopTrig.setROSpecStopTriggerType(new ROSpecStopTriggerType(ROSpecStopTriggerType.Null));
roBoundarySpec.setROSpecStopTrigger(stopTrig);
roSpec.setROBoundarySpec(roBoundarySpec);
// Add an Antenna Inventory Spec (AISpec).
AISpec aispec = new AISpec();
// Set the AI stop trigger to null. This means that
// the AI spec will run until the ROSpec stops.
AISpecStopTrigger aiStopTrigger = new AISpecStopTrigger();
aiStopTrigger
.setAISpecStopTriggerType(new AISpecStopTriggerType(AISpecStopTriggerType.Null));
aiStopTrigger.setDurationTrigger(new UnsignedInteger(0));
aispec.setAISpecStopTrigger(aiStopTrigger);
// Select which antenna ports we want to use.
// Setting this property to zero means all antenna ports.
UnsignedShortArray antennaIDs = new UnsignedShortArray();
antennaIDs.add(new UnsignedShort(0));
aispec.setAntennaIDs(antennaIDs);
// Tell the reader that we're reading Gen2 tags.
InventoryParameterSpec inventoryParam = new InventoryParameterSpec();
inventoryParam.setProtocolID(new AirProtocols(AirProtocols.EPCGlobalClass1Gen2));
inventoryParam.setInventoryParameterSpecID(new UnsignedShort(1));
roSpec.addToSpecParameterList(aispec);
AntennaConfiguration antConfig = new AntennaConfiguration();
antConfig.setAntennaID(new UnsignedShort(0));
RFTransmitter tx = new RFTransmitter();
tx.setTransmitPower(new UnsignedShort(87)); // TODO: Is this the max?
tx.setChannelIndex(new UnsignedShort(1));
tx.setHopTableID(new UnsignedShort(1));
antConfig.setRFTransmitter(tx);
inventoryParam.addToAntennaConfigurationList(antConfig);
aispec.addToInventoryParameterSpecList(inventoryParam);
// Specify what type of tag reports we want
// to receive and when we want to receive them.
ROReportSpec roReportSpec = new ROReportSpec();
// Receive a report every time a tag is read.
roReportSpec.setROReportTrigger(new ROReportTriggerType(
ROReportTriggerType.Upon_N_Tags_Or_End_Of_ROSpec));
roReportSpec.setN(new UnsignedShort(1));
TagReportContentSelector reportContent = new TagReportContentSelector();
// Select which fields we want in the report.
reportContent.setEnableAccessSpecID(new Bit(0));
reportContent.setEnableAntennaID(new Bit(0));
reportContent.setEnableChannelIndex(new Bit(0));
reportContent.setEnableFirstSeenTimestamp(new Bit(0));
reportContent.setEnableInventoryParameterSpecID(new Bit(0));
reportContent.setEnableLastSeenTimestamp(new Bit(1));
reportContent.setEnablePeakRSSI(new Bit(0));
reportContent.setEnableROSpecID(new Bit(0));
reportContent.setEnableSpecIndex(new Bit(0));
reportContent.setEnableTagSeenCount(new Bit(0));
roReportSpec.setTagReportContentSelector(reportContent);
roSpec.setROReportSpec(roReportSpec);
return roSpec;
}
public List<TransmitPowerLevelTableEntry> readTransmitPowerEntries() throws LLRPException {
GET_READER_CAPABILITIES_RESPONSE response;
try {
GET_READER_CAPABILITIES getReaderCaps = new GET_READER_CAPABILITIES();
getReaderCaps.setRequestedData(new GetReaderCapabilitiesRequestedData(GetReaderCapabilitiesRequestedData.All));
response = (GET_READER_CAPABILITIES_RESPONSE) reader.transact(getReaderCaps, TIMEOUT_MS);
StatusCode status = response.getLLRPStatus().getStatusCode();
if (status.equals(new StatusCode(StatusCode.M_Success))) {
return response.getRegulatoryCapabilities().getUHFBandCapabilities().getTransmitPowerLevelTableEntryList();
} else {
LOG.error("Error reading capabilities. Status code: {}", status.toString());
throw new LLRPException();
}
} catch (TimeoutException e) {
LOG.error("Timeout when adding reading capabilities.", e);
throw new LLRPException(e);
}
}
// Add the ROSpec to the reader.
public void addROSpec() throws LLRPException {
ADD_ROSPEC_RESPONSE response;
ROSpec roSpec = buildROSpec();
LOG.info("Adding the ROSpec");
try {
ADD_ROSPEC roSpecMsg = new ADD_ROSPEC();
roSpecMsg.setROSpec(roSpec);
response = (ADD_ROSPEC_RESPONSE) reader.transact(roSpecMsg, TIMEOUT_MS);
LOG.info("Adding ROSpec response: {}", response.toXMLString());
// Check if the we successfully added the ROSpec.
StatusCode status = response.getLLRPStatus().getStatusCode();
if (status.equals(new StatusCode(StatusCode.M_Success))) {
LOG.info("Successfully added ROSpec.");
} else {
// TODO: use the status code, Luke!
LOG.error("Error adding ROSpec. Status code: {}", status.toString());
throw new LLRPException();
}
} catch (TimeoutException e) {
LOG.error("Timeout when adding ROSpec.", e);
throw new LLRPException(e);
} catch (InvalidLLRPMessageException e) {
LOG.fatal("Formed invalid ADD_ROSPEC message.", e);
throw new LLRPException(e);
}
}
// Enable the ROSpec.
public void enableROSpec() throws LLRPException {
ENABLE_ROSPEC_RESPONSE response;
LOG.info("Enabling the ROSpec.");
ENABLE_ROSPEC enable = new ENABLE_ROSPEC();
enable.setROSpecID(new UnsignedInteger(ROSPEC_ID));
try {
response = (ENABLE_ROSPEC_RESPONSE) reader.transact(enable, TIMEOUT_MS);
LOG.info("ROSpec enable response: {}", response.toXMLString());
} catch (TimeoutException e) {
LOG.error("Timeout when enabling ROSpec.", e);
throw new LLRPException(e);
} catch (InvalidLLRPMessageException e) {
LOG.fatal("Formed invalid ENABLE_ROSPEC message.", e);
throw new LLRPException(e);
}
}
// Start the ROSpec.
public void startROSpec() throws LLRPException {
START_ROSPEC_RESPONSE response;
LOG.info("Starting the ROSpec.");
START_ROSPEC start = new START_ROSPEC();
start.setROSpecID(new UnsignedInteger(ROSPEC_ID));
try {
response = (START_ROSPEC_RESPONSE) reader.transact(start, TIMEOUT_MS);
LOG.info("Start ROSpec response: {}", response.toXMLString());
} catch (TimeoutException e) {
LOG.error("Timeout when starting ROSpec.", e);
throw new LLRPException(e);
} catch (InvalidLLRPMessageException e) {
LOG.fatal("Formed invalid START_ROSPEC message.", e);
throw new LLRPException(e);
}
}
// Delete all ROSpecs from the reader.
public void deleteROSpecs() throws LLRPException {
DELETE_ROSPEC_RESPONSE response;
LOG.info("Deleting all ROSpecs.");
DELETE_ROSPEC del = new DELETE_ROSPEC();
// Use zero as the ROSpec ID.
// This means delete all ROSpecs.
del.setROSpecID(new UnsignedInteger(0));
try {
response = (DELETE_ROSPEC_RESPONSE) reader.transact(del, TIMEOUT_MS);
LOG.info("Delete ROSpec response: {}", response.toXMLString());
} catch (TimeoutException e) {
LOG.error("Timeout when deleting ROSpec.", e);
throw new LLRPException(e);
} catch (InvalidLLRPMessageException e) {
LOG.fatal("Formed invalid DELETE_ROSPEC message.", e);
throw new LLRPException(e);
}
}
// Connect to the reader
public void connect(URI uri) throws LLRPException {
// Create the reader object.
if (uri.getPort() == -1) {
reader = new LLRPConnector(this, uri.getHost());
} else {
reader = new LLRPConnector(this, uri.getHost(), uri.getPort());
}
// Try connecting to the reader.
try {
LOG.info("Connecting to the reader.");
// NOTE: The timeout is a lot longer
reader.connect(CONNECT_TIMEOUT);
} catch (LLRPConnectionAttemptFailedException e1) {
LOG.error("Error connecting to the reader", e1);
throw new LLRPException(e1);
}
}
// Disconnect from the reader
public void disconnect() {
reader.disconnect();
}
// Connect to the reader, setup the ROSpec
// and run it.
public boolean run(URI uri) {
try {
connect(uri);
deleteROSpecs();
addROSpec();
enableROSpec();
startROSpec();
return true;
} catch (LLRPException e) {
return false;
}
}
// Cleanup. Delete all ROSpecs
// and disconnect from the reader.
public boolean stop() {
try {
deleteROSpecs();
disconnect();
return true;
} catch (LLRPException e) {
return false;
}
}
@Override
public void messageReceived(LLRPMessage message) {
messageConsumer.accept(message);
}
@Override
public void errorOccured(String message) {
errorConsumer.accept(message);
}
public static TagId decodeEPCParameter(EPCParameter epc) {
// TODO: Use LLRPBitList sublist stuff?
byte[] epcBytes = epc.encodeBinary().toByteArray();
if (epcBytes[0] == ((byte)0x8d)) {
return new TagId(decodeEPC96(epcBytes));
} else if (epcBytes[1] == ((byte)0xf1)) {
return new TagId(decodeEPCData(epcBytes));
} else {
LOG.error("Couldn't decode EPCParameter {}: unknown format.", new TagId(epcBytes));
return new TagId(epcBytes);
}
}
private static byte[] decodeEPC96(byte[] epc96) {
byte[] epc = new byte[12]; // 96 bits is 12 bytes
System.arraycopy(epc96, 1, epc, 0, 12);
return epc;
}
private static byte[] decodeEPCData(byte[] epcData) {
int length = 0;
length += Byte.toUnsignedInt(epcData[4]) << 8;
length += Byte.toUnsignedInt(epcData[5]);
// TODO: What if EPC length is not a multiple of 8? Does this occur?
byte[] epc = new byte[length / 8];
System.arraycopy(epcData, 6, epc, 0, length / 8);
return epc;
}
}