package org.yamcs.tctm; import java.io.IOException; import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.ConfigurationException; import org.yamcs.YConfiguration; import org.yamcs.YamcsServer; import org.yamcs.cmdhistory.CommandHistoryPublisher; import org.yamcs.commanding.PreparedCommand; import org.yamcs.parameter.ParameterValue; import org.yamcs.parameter.SystemParametersCollector; import org.yamcs.parameter.SystemParametersProducer; import org.yamcs.protobuf.Commanding.CommandId; import org.yamcs.time.TimeService; import org.yamcs.utils.LoggingUtils; import org.yamcs.utils.TimeEncoding; import com.google.common.util.concurrent.AbstractService; /** * Sends raw packets on Tcp socket. * @author nm * */ public class TcpTcDataLink extends AbstractService implements Runnable, TcDataLink, SystemParametersProducer { protected SocketChannel socketChannel=null; protected String host="whirl"; protected int port=10003; protected CommandHistoryPublisher commandHistoryListener; protected Selector selector; SelectionKey selectionKey; protected CcsdsSeqAndChecksumFiller seqAndChecksumFiller=new CcsdsSeqAndChecksumFiller(); protected ScheduledThreadPoolExecutor timer; protected volatile boolean disabled=false; protected int minimumTcPacketLength = -1; //the minimum size of the CCSDS packets uplinked protected volatile long tcCount; private String sv_linkStatus_id, sp_dataCount_id; private SystemParametersCollector sysParamCollector; protected final Logger log; private String yamcsInstance; private String name; TimeService timeService; public TcpTcDataLink(String yamcsInstance, String name, String spec) throws ConfigurationException { log = LoggingUtils.getLogger(this.getClass(), yamcsInstance); YConfiguration c = YConfiguration.getConfiguration("tcp"); this.yamcsInstance = yamcsInstance; host = c.getString(spec, "tcHost"); port = c.getInt(spec, "tcPort"); this.name = name; if(c.containsKey(spec, "minimumTcPacketLength")) { minimumTcPacketLength = c.getInt(spec, "minimumTcPacketLength"); } else { log.debug("minimumTcPacketLength not defined, using the default value {}", minimumTcPacketLength); } timeService = YamcsServer.getTimeService(yamcsInstance); } protected TcpTcDataLink() { log=LoggerFactory.getLogger(this.getClass().getName()); } // dummy constructor which is automatically invoked by subclass constructors public TcpTcDataLink(String host, int port) { this.host=host; this.port=port; openSocket(); log=LoggerFactory.getLogger(this.getClass().getName()); } protected long getCurrentTime() { if(timeService!=null) { return timeService.getMissionTime(); } else { return TimeEncoding.fromUnixTime(System.currentTimeMillis()); } } @Override protected void doStart() { setupSysVariables(); this.timer=new ScheduledThreadPoolExecutor(1); timer.scheduleWithFixedDelay(this, 0, 10, TimeUnit.SECONDS); notifyStarted(); } protected void openSocket() { try { InetAddress address=InetAddress.getByName(host); socketChannel=SocketChannel.open(new InetSocketAddress(address,port)); socketChannel.configureBlocking(false); socketChannel.socket().setKeepAlive(true); selector = Selector.open(); selectionKey = socketChannel.register(selector,SelectionKey.OP_WRITE|SelectionKey.OP_READ); log.info("TC connection established to {}:{}", host, port); } catch (IOException e) { String exc = (e instanceof ConnectException) ? ((ConnectException) e).getMessage() : e.toString(); log.info("Cannot open TC connection to {}:{} '{}'. Retrying in 10s", host, port, exc.toString()); try { socketChannel.close(); } catch (Exception e1) {} try { selector.close(); } catch (Exception e1) {} socketChannel=null; } } protected void disconnect() { if(socketChannel==null) { return; } try { socketChannel.close(); selector.close(); socketChannel=null; } catch (IOException e) { log.warn("Exception caught when checking if the socket to {}:{} is open", host, port, e); } } /** * we check if the socket is open by trying a select on the read part of it * @return */ protected boolean isSocketOpen() { final ByteBuffer bb=ByteBuffer.allocate(16); if(socketChannel==null) { return false; } boolean connected=false; try { selector.select(); if(selectionKey.isReadable()) { int read = socketChannel.read(bb); if(read>0) { log.info("Data read on the TC socket to {}:{}!! : {}",host, port, bb); connected=true; } else if(read<0) { log.warn("TC socket to "+host+":"+port+" has been closed"); socketChannel.close(); selector.close(); socketChannel=null; connected=false; } } else if(selectionKey.isWritable()){ connected=true; } else { log.warn("The TC socket to "+host+":"+port+" is neither writable nor readable"); connected=false; } } catch (IOException e) { log.warn("Exception caught when checking if the socket to {}:{} is open:",host, port, e); connected=false; } return connected; } /** * Sends */ @Override public void sendTc(PreparedCommand pc) { if(disabled) { log.warn("TC disabled, ignoring command "+pc.getCommandId()); return; } ByteBuffer bb = null; if(pc.getBinary().length<minimumTcPacketLength) { //enforce the minimum packet length bb=ByteBuffer.allocate(minimumTcPacketLength); bb.put(pc.getBinary()); bb.putShort(4, (short)(minimumTcPacketLength - 7)); // fix packet length } else { bb=ByteBuffer.wrap(pc.getBinary()); } int retries=5; boolean sent=false; int seqCount=seqAndChecksumFiller.fill(bb, pc.getCommandId().getGenerationTime()); bb.rewind(); while (!sent&&(retries>0)) { if (!isSocketOpen()) { openSocket(); } if(isSocketOpen()) { try { socketChannel.write(bb); tcCount++; sent=true; } catch (IOException e) { log.warn("Error writing to TC socket to {}:{} : {}", host, port, e.getMessage()); try { if(socketChannel.isOpen()) { socketChannel.close(); } selector.close(); socketChannel = null; } catch (IOException e1) { e1.printStackTrace(); } } } retries--; if(!sent && (retries>0)) { try { log.warn("Command not sent, retrying in 2 seconds"); Thread.sleep(2000); } catch (InterruptedException e) { log.warn("exception {} thrown when sleeping 2 sec", e.toString()); Thread.currentThread().interrupt(); } } } if(sent) { handleAcks(pc.getCommandId(), seqCount); } else { timer.schedule(new TcAckStatus(pc.getCommandId(), "Acknowledge_FSC_Status","NACK"), 100, TimeUnit.MILLISECONDS); } } protected void handleAcks(CommandId cmdId, int seqCount ) { timer.schedule(new TcAck(cmdId,"Final_Sequence_Count", Integer.toString(seqCount)), 200, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_FSC","ACK: OK"), 400, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_FRC","ACK: OK"), 800, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_DASS","ACK: OK"), 1200, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_MCS","ACK: OK"), 1600, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_A","ACK A: OK"), 2000, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_B","ACK B: OK"), 3000, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_C","ACK C: OK"), 4000, TimeUnit.MILLISECONDS); timer.schedule(new TcAckStatus(cmdId,"Acknowledge_D","ACK D: OK"), 10000, TimeUnit.MILLISECONDS); } @Override public void setCommandHistoryPublisher(CommandHistoryPublisher commandHistoryListener) { this.commandHistoryListener=commandHistoryListener; } @Override public String getLinkStatus() { if (disabled) { return "DISABLED"; } if(isSocketOpen()) { return "OK"; } else { return "UNAVAIL"; } } @Override public String getDetailedStatus() { if(disabled) return String.format("DISABLED (should connect to %s:%d)", host, port); if(isSocketOpen()) { return String.format("OK, connected to %s:%d", host, port); } else { return String.format("Not connected to %s:%d", host, port); } } @Override public void disable() { disabled=true; if(isRunning()) { disconnect(); } } @Override public void enable() { disabled=false; } @Override public boolean isDisabled() { return disabled; } @Override public void run() { if(!isRunning() || disabled) { return; } if (!isSocketOpen()) { openSocket(); } } @Override public void doStop() { disconnect(); notifyStopped(); } class TcAck implements Runnable { CommandId cmdId; String name; String value; TcAck(CommandId cmdId, String name, String value) { this.cmdId=cmdId; this.name=name; this.value=value; } @Override public void run() { commandHistoryListener.updateStringKey(cmdId,name,value); } } public class TcAckStatus extends TcAck { public TcAckStatus(CommandId cmdId, String name, String value) { super(cmdId, name, value); } @Override public void run() { long instant = getCurrentTime(); commandHistoryListener.updateStringKey(cmdId,name+"_Status",value); commandHistoryListener.updateTimeKey(cmdId,name+"_Time", instant); } } @Override public long getDataCount() { return tcCount; } protected void setupSysVariables() { this.sysParamCollector = SystemParametersCollector.getInstance(yamcsInstance); if(sysParamCollector!=null) { sysParamCollector.registerProvider(this, null); sv_linkStatus_id = sysParamCollector.getNamespace()+"/"+name+"/linkStatus"; sp_dataCount_id = sysParamCollector.getNamespace()+"/"+name+"/dataCount"; } else { log.info("System variables collector not defined for instance {} ", yamcsInstance); } } @Override public Collection<ParameterValue> getSystemParameters() { long time = getCurrentTime(); ParameterValue linkStatus = SystemParametersCollector.getPV(sv_linkStatus_id, time, getLinkStatus()); ParameterValue dataCount = SystemParametersCollector.getPV(sp_dataCount_id, time, getDataCount()); return Arrays.asList(linkStatus, dataCount); } public int getMiniminimumTcPacketLength() { return minimumTcPacketLength; } }