All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.yamcs.tctm.UdpParameterDataLink Maven / Gradle / Ivy

There is a newer version: 5.10.9
Show newest version
package org.yamcs.tctm;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.yamcs.Spec;
import org.yamcs.Spec.OptionType;
import org.yamcs.YConfiguration;
import org.yamcs.parameter.BasicParameterValue;
import org.yamcs.parameter.ParameterValue;
import org.yamcs.protobuf.Pvalue;
import org.yamcs.protobuf.Pvalue.ParameterData;
import org.yamcs.protobuf.Yamcs.NamedObjectId;

import com.google.protobuf.util.JsonFormat;

/**
 * Receives PP data via UDP.
 * 
 * The UDP packets are protobuf encoded ParameterData. We don't use any checksum, assume it's done by UDP.
 * 
 * @author nm
 *
 */
public class UdpParameterDataLink extends AbstractParameterDataLink implements Runnable {

    private volatile int validDatagramCount = 0;
    private volatile int invalidDatagramCount = 0;

    private int sequenceCount = 0;

    private DatagramSocket udpSocket;
    private int port = 31002;
    private String defaultRecordingGroup;
    private Format format;

    int MAX_LENGTH = 10 * 1024;

    DatagramPacket datagram = new DatagramPacket(new byte[MAX_LENGTH], MAX_LENGTH);

    @Override
    public Spec getSpec() {
        var spec = getDefaultSpec();
        spec.addOption("port", OptionType.INTEGER).withRequired(true);
        spec.addOption("recordingGroup", OptionType.STRING).withDefault("DEFAULT");
        spec.addOption("json", OptionType.BOOLEAN).withDefault(false);
        return spec;
    }

    @Override
    public void init(String instance, String name, YConfiguration config) {
        super.init(instance, name, config);
        port = config.getInt("port");
        defaultRecordingGroup = config.getString("recordingGroup", "DEFAULT");
        format = config.getBoolean("json", false) ? Format.JSON : Format.PROTOBUF;
    }

    @Override
    protected void doStart() {
        if (!isDisabled()) {
            try {
                udpSocket = new DatagramSocket(port);
                Thread thread = new Thread(this);
                thread.setName(getClass().getSimpleName() + "-" + linkName);
                thread.start();
            } catch (SocketException e) {
                notifyFailed(e);
                return;
            }
        }
        notifyStarted();
    }

    @Override
    protected void doStop() {
        if (udpSocket != null) {
            udpSocket.close();
        }
        notifyStopped();
    }

    @Override
    public void run() {
        while (isRunningAndEnabled()) {
            ParameterData pdata = getNextData();
            if (pdata == null) {
                continue;
            }

            if (pdata.hasGenerationTime()) {
                log.error("Generation time must be specified for each parameter separately");
                continue;
            }

            long now = timeService.getMissionTime();
            String recgroup = pdata.hasGroup() ? pdata.getGroup() : defaultRecordingGroup;
            int sequenceNumber = pdata.hasSeqNum() ? pdata.getSeqNum() : sequenceCount++;

            // Regroup by gentime, just in case multiple parameters are submitted with different times.
            Map> valuesByTime = new LinkedHashMap<>();

            for (Pvalue.ParameterValue gpv : pdata.getParameterList()) {
                NamedObjectId id = gpv.getId();
                if (id == null) {
                    log.warn("parameter without id, skipping");
                    continue;
                }
                String fqn = id.getName();
                if (id.hasNamespace()) {
                    log.trace("Using namespaced name for parameter {} because fully qualified name not available.", id);
                }
                ParameterValue pv = BasicParameterValue.fromGpb(fqn, gpv);
                long gentime = gpv.hasGenerationTime() ? pv.getGenerationTime() : now;
                pv.setGenerationTime(gentime);

                List pvals = valuesByTime.computeIfAbsent(gentime, x -> new ArrayList<>());
                pvals.add(pv);
            }

            for (Entry> group : valuesByTime.entrySet()) {
                updateParameters((long) group.getKey(), recgroup, sequenceNumber, group.getValue());
            }
        }
    }

    /**
     * 
     * Called to retrieve the next packet. It blocks in reading on the UDP 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 ParameterData getNextData() {
        while (isRunning()) {
            try {
                udpSocket.receive(datagram);
                validDatagramCount++;
                ParameterData pd = decodeDatagram(datagram.getData(), datagram.getOffset(), datagram.getLength());
                dataIn(pd.getParameterCount(), datagram.getLength());

                return pd;
            } catch (IOException e) {
                // Shutdown or disable will close the socket. That generates an exception
                // which we ignore here.
                if (!isRunning() || isDisabled()) {
                    return null;
                }
                log.warn("Exception when receiving parameter data: {}'", e.toString());
                dataIn(0, datagram.getLength());
                invalidDatagramCount++;
            }
        }
        return null;
    }

    /**
     * Decode {@link ParameterData} from the content of a single received UDP Datagram.
     * 

* {@link UdpParameterDataLink} has configurable support for either Protobuf or JSON-encoded data. Extending links * may provide a custom decoder by overriding this method. * * @param data * data buffer. The data received starts from {@code offset} and runs for {@code length} long. * @param offset * offset of the data received * @param length * length of the data received */ public ParameterData decodeDatagram(byte[] data, int offset, int length) throws IOException { switch (format) { case JSON: try (Reader reader = new InputStreamReader(new ByteArrayInputStream(data, offset, length))) { ParameterData.Builder builder = ParameterData.newBuilder(); JsonFormat.parser().merge(reader, builder); return builder.build(); } case PROTOBUF: return ParameterData.newBuilder() .mergeFrom(data, offset, length) .build(); default: throw new IllegalStateException("Unexpected format " + format); } } @Override public Status connectionStatus() { return Status.OK; } @Override public String getDetailedStatus() { if (isDisabled()) { return "DISABLED (should receive on " + port + ")"; } else { return "OK, receiving on " + port; } } @Override public Map getExtraInfo() { var extra = new LinkedHashMap(); extra.put("Valid datagrams", validDatagramCount); extra.put("Invalid datagrams", invalidDatagramCount); return extra; } @Override protected void doEnable() throws Exception { udpSocket = new DatagramSocket(port); Thread thread = new Thread(this); thread.setName(getClass().getSimpleName() + "-" + linkName); thread.start(); } @Override protected void doDisable() throws Exception { if (udpSocket != null) { udpSocket.close(); udpSocket = null; } } @Override public void resetCounters() { super.resetCounters(); validDatagramCount = 0; invalidDatagramCount = 0; } /** * Default supported data formats */ private static enum Format { JSON, PROTOBUF; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy