package org.yamcs.tctm; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.nio.ByteBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.ConfigurationException; import org.yamcs.YConfiguration; import org.yamcs.archive.PacketWithTime; import com.google.common.util.concurrent.AbstractExecutionThreadService; import org.yamcs.utils.CcsdsPacket; import org.yamcs.utils.TimeEncoding; /** * Receives telemetry packets via multicast from TMR. The nice thing about multicast is that there is no * connection required so the code can be fairly simple. * Keeps simple statistics about the number of datagram received and the number of too short datagrams * @author nm * */ public class MulticastTmDataLink extends AbstractExecutionThreadService implements TmPacketDataLink { private volatile int validDatagramCount = 0; private volatile int invalidDatagramCount = 0; private volatile boolean disabled=false; private volatile boolean quitting=false; private MulticastSocket tmSocket; private String group="239.192.0.1"; private int port=31002; private TmSink tmSink; private Logger log=LoggerFactory.getLogger(this.getClass().getName()); final int maxLength=1500; //maximum length of tm packets in columbus is 1472 DatagramPacket datagram = new DatagramPacket(new byte[maxLength], maxLength); /** * Creates a * @param spec * @throws ConfigurationException if tmGroup or tmPort are not defined in the configuration files */ public MulticastTmDataLink(String instance, String name, String spec) throws ConfigurationException { YConfiguration c=YConfiguration.getConfiguration("multicast"); group=c.getString(spec, "tmGroup"); port=c.getInt(spec, "tmPort"); try { openSocket(); } catch (IOException e) { throw new ConfigurationException("IOException caught when opening the multicast socket: "+e); } } /** * Creates a simple multicast receiver listening to the given group and port * @param group * @param port * @throws IOException if there was an exception opening the port (happends when there is alredy another process bound * to that port and the option REUSE_ADDR is not set on its socket) */ public MulticastTmDataLink(String group, int port) throws IOException { this.group=group; this.port=port; openSocket(); } private void openSocket() throws IOException { tmSocket=new MulticastSocket(port); tmSocket.joinGroup(InetAddress.getByName(group)); } @Override public void setTmSink(TmSink tmSink) { this.tmSink=tmSink; } @Override public void run() { while(!quitting) { PacketWithTime pwrt=getNextPacket(); tmSink.processPacket(pwrt); } } /** * * Called to retrieve the next packet. * It blocks in readining on the multicast socket * @return anything that looks as a valid packet, just the size is taken into account to decide if it's valid or not */ public PacketWithTime getNextPacket() { ByteBuffer packet = null; while(disabled) { try { Thread.sleep(1000); } catch (InterruptedException e) { return null; } } long rectime=TimeEncoding.INVALID_INSTANT; while (isRunning()) { try { tmSocket.receive(datagram); /*the packet received from TMR has a 10 bytes header followed by the CCSDS packet: * some kind of identification always 0x0B for TM and 06 for PP - 1 byte - the list with all the packet types can be found in the DaSS_C_Packet_Decoder.java part of the DaSS API * ccsds time - 5 bytes * requestId (always set to 0 (I guess only for multicast) - 4 bytes * ccsds packet * * The time in the "TMR header" is the reception time. It looks like the CCSDS GPS but is generated locally so it's UNIX time in fact */ if(datagram.getLength()<26) { //10 for the TMR header plus 6 for the primary CCSDS header plus 10 for secondary CCSDS header log.warn("Incomplete packet received on the multicast, discarded: {}", datagram); continue; } byte[] data = datagram.getData(); int offset = datagram.getOffset(); ByteBuffer bb = ByteBuffer.wrap(data); //the time sent by TMR is not really GPS, it's the unix local computer time shifted to GPS epoch long unixTimesec=(0xFFFFFFFFL & (long)bb.getInt(offset+1))+315964800L; int unixTimeMicrosec=(0xFF&bb.get(offset+5))*(1000000/256); rectime = TimeEncoding.fromUnixTime(unixTimesec, unixTimeMicrosec); int pktLength = 7+((data[14+offset]&0xFF)<<8)+(data[15+offset]&0xFF); if(pktLength<16) { invalidDatagramCount++; log.warn("Invalid packet received on the multicast, pktLength: {}. Expecting minimum 16 bytes", pktLength); continue; } if(datagram.getLength()<10+pktLength) { invalidDatagramCount++; log.warn("Incomplete packet received on the multicast. expected {}, received: {}", pktLength, (datagram.getLength()-10)); continue; } validDatagramCount++; packet = ByteBuffer.allocate(pktLength); packet.put(data, offset+10, pktLength); break; } catch (IOException e) { log.warn("exception {} thrown when reading from the multicast socket {}:{}", group, port, e); } } if(packet!=null) { return new PacketWithTime(rectime, CcsdsPacket.getInstant(packet), packet.array()); } else { return null; } } @Override public String getLinkStatus() { return disabled?"DISABLED":"OK"; } /** * returns statistics with the number of datagram received and the number of invalid datagrams */ @Override public String getDetailedStatus() { if(disabled) { return "DISABLED"; } else { return String.format("OK (%s:%d)%nValid datagrams received: %d%nInvalid datagrams received: %d", group, port, validDatagramCount, invalidDatagramCount); } } /** * Sets the disabled to true such that getNextPacket ignores the received datagrams */ @Override public void disable() { disabled=true; } /** * Sets the disabled to false such that getNextPacket does not ignore the received datagrams */ @Override public void enable() { disabled=false; } @Override public boolean isDisabled() { return disabled; } @Override public long getDataCount() { return validDatagramCount; } }