package org.yamcs.tctm;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.yamcs.ConfigurationException;
import org.yamcs.YConfiguration;
import org.yamcs.YamcsServer;
import org.yamcs.archive.PacketWithTime;
import org.yamcs.parameter.ParameterValue;
import org.yamcs.parameter.SystemParametersCollector;
import org.yamcs.parameter.SystemParametersProducer;
import org.yamcs.time.TimeService;
import org.yamcs.utils.CcsdsPacket;
import org.yamcs.utils.LoggingUtils;
import com.google.common.util.concurrent.AbstractExecutionThreadService;
public class TcpTmDataLink extends AbstractExecutionThreadService implements TmPacketDataLink, SystemParametersProducer {
protected volatile long packetcount = 0;
protected Socket tmSocket;
protected String host="localhost";
protected int port=10031;
protected volatile boolean disabled=false;
protected final Logger log;
private TmSink tmSink;
private SystemParametersCollector sysParamCollector;
ParameterValue svConnectionStatus;
List<ParameterValue> sysVariables= new ArrayList<>();
private String spLinkStatus, spDataCount;
final String yamcsInstance;
final String name;
final protected TimeService timeService;
protected TcpTmDataLink(String instance, String name) {// dummy constructor needed by subclass constructors
this.yamcsInstance = instance;
this.name = name;
this.timeService = YamcsServer.getTimeService(instance);
log = LoggingUtils.getLogger(this.getClass(), instance);
}
public TcpTmDataLink(String instance, String name, String spec) throws ConfigurationException {
this(instance, name);
YConfiguration c=YConfiguration.getConfiguration("tcp");
host=c.getString(spec, "tmHost");
port=c.getInt(spec, "tmPort");
}
protected void openSocket() throws IOException {
InetAddress address=InetAddress.getByName(host);
tmSocket=new Socket();
tmSocket.setKeepAlive(true);
tmSocket.connect(new InetSocketAddress(address,port),1000);
}
@Override
public void setTmSink(TmSink tmSink) {
this.tmSink=tmSink;
}
@Override
public void run() {
setupSysVariables();
while(isRunning()) {
PacketWithTime pwrt=getNextPacket();
if(pwrt==null) {
break;
}
tmSink.processPacket(pwrt);
}
}
public PacketWithTime getNextPacket() {
ByteBuffer bb=null;
while (isRunning()) {
while(disabled) {
if(!isRunning()) {
return null;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
try {
if (tmSocket==null) {
openSocket();
log.info("TM connection established to {}:{}", host, port);
}
byte hdr[] = new byte[6];
if(!readWithBlocking(hdr,0,6))
continue;
int remaining=((hdr[4]&0xFF)<<8)+(hdr[5]&0xFF)+1;
bb=ByteBuffer.allocate(6+remaining).put(hdr);
if(!readWithBlocking(bb.array(), 6, remaining))
continue;
bb.rewind();
packetcount++;
break;
} catch (IOException e) {
String exc = (e instanceof ConnectException) ? ((ConnectException) e).getMessage() : e.toString();
log.info("Cannot open or read TM socket {}:{} {}'. Retrying in 10s", host, port, exc);
try {
tmSocket.close();
} catch (Exception e2) {}
tmSocket=null;
try {
Thread.sleep(10000);
} catch (InterruptedException e1) {
log.warn("Exception {} while sleeping for 10s", e1.toString());
return null;
}
}
}
if(bb!=null) {
return new PacketWithTime(timeService.getMissionTime(), CcsdsPacket.getInstant(bb), bb.array());
}
return null;
}
/**
* Read n bytes from the tmSocket, blocking if necessary till all bytes are available.
* Returns true if all the bytes have been read and false if the stream has closed before all the bytes have been read.
* @param b
* @param n
* @return
* @throws IOException
*/
protected boolean readWithBlocking(byte[] b, int pos, int n) throws IOException {
InputStream in=tmSocket.getInputStream();
int remaining=n;
while(remaining>0) {
int read=in.read(b,pos,remaining);
if(read==-1) {
log.warn("Tm Connection closed");
if(remaining!=n) {
log.warn("Discarding incomplete TM packet read: expected "+n+", read"+(n-remaining)+". Packet discarded.");
}
tmSocket=null;
return false;
}
remaining-=read;
pos+=read;
}
return true;
}
@Override
public String getLinkStatus() {
if (disabled) {
return "DISABLED";
}
if (tmSocket==null) {
return "UNAVAIL";
} else {
return "OK";
}
}
@Override
public void triggerShutdown() {
if(tmSocket!=null) {
try {
tmSocket.close();
} catch (IOException e) {
log.warn("Exception got when closing the tm socket:", e);
}
tmSocket=null;
}
}
@Override
public void disable() {
disabled=true;
if(tmSocket!=null) {
try {
tmSocket.close();
} catch (IOException e) {
log.warn("Exception got when closing the tm socket:", e);
}
tmSocket=null;
}
}
@Override
public void enable() {
disabled=false;
}
@Override
public boolean isDisabled() {
return disabled;
}
@Override
public String getDetailedStatus() {
if(disabled) {
return String.format("DISABLED (should connect to %s:%d)", host, port);
}
if (tmSocket==null) {
return String.format("Not connected to %s:%d", host, port);
} else {
return String.format("OK, connected to %s:%d, received %d packets", host, port, packetcount);
}
}
@Override
public long getDataCount() {
return packetcount;
}
protected void setupSysVariables() {
this.sysParamCollector = SystemParametersCollector.getInstance(yamcsInstance);
if(sysParamCollector!=null) {
sysParamCollector.registerProvider(this, null);
spLinkStatus = sysParamCollector.getNamespace()+"/"+name+"/linkStatus";
spDataCount = sysParamCollector.getNamespace()+"/"+name+"/dataCount";
} else {
log.info("System variables collector not defined for instance {} ", yamcsInstance);
}
}
@Override
public Collection<ParameterValue> getSystemParameters() {
long time = timeService.getMissionTime();
ParameterValue linkStatus = SystemParametersCollector.getPV(spLinkStatus, time, getLinkStatus());
ParameterValue dataCount = SystemParametersCollector.getPV(spDataCount, time, getDataCount());
return Arrays.asList(linkStatus, dataCount);
}
}