diff --git a/README.md b/README.md index a5c3035..1ea93e7 100644 --- a/README.md +++ b/README.md @@ -44,27 +44,35 @@ Generate random data for development of the frontend. Without the simulator, we - Real-time data synchronization with React components - Error handling and retry logic -## 🛠️ Prerequisites - -- **Node.js** 22+ -- **pnpm** 10.26.1+ -- **Maven** 3.9+ -- **Docker** & **Docker Compose** -- **Tilt** (for development orchestration) - ## 📋 Installation +Install the required tools: +- [Node.js](https://nodejs.org/en/download) +- [pnpm](https://pnpm.io/installation) +- [Java (JDK)](https://www.oracle.com/ca-en/java/technologies/downloads/) +- [Maven](https://maven.apache.org/) +- [Tilt](https://docs.tilt.dev/install.html) +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) + +or if you're on MacOS, install with Homebrew +```bash +brew install node pnpm java maven tilt +brew install --cask docker-desktop +``` +Then clone the repository and install the dependencies locally. ```bash # Clone the repository git clone https://github.com/McGillRocketTeam/ground-station cd ground-station # Install dependencies for all packages pnpm install +# Build initial packages +pnpm build ``` ## 🏃‍♂️ Running Everything with Tilt -**Tilt** orchestrates the entire development environment, including all Docker services and applications. +**Tilt** orchestrates the entire development environment, including all Docker services and applications. **Ensure that Docker Desktop is running before you start Tilt.** ```bash # Start all services and applications @@ -94,12 +102,6 @@ pnpm --filter @mrt/frontend dev YAMCS_INSTANCE=ground_station pnpm --filter @mrt/simulator dev ``` -### Build All Packages - -```bash -pnpm turbo run build -``` - ### Type Checking ```bash diff --git a/Tiltfile b/Tiltfile index 79acd40..6f09ba4 100644 --- a/Tiltfile +++ b/Tiltfile @@ -14,7 +14,6 @@ local_resource( resource_deps=['backend', 'yamcs-effect', 'yamcs-atom'] ) - local_resource( 'backend', serve_cmd="cd apps/backend && mvn yamcs:run", @@ -49,7 +48,7 @@ local_resource( } .\\venv\\Scripts\\pip install -r requirements.txt - .\\venv\\Scripts\\python converter.py --output "../backend/src/main/yamcs/mdb/rocket.xml" + .\\venv\\Scripts\\python ./src/xtce_generator.py --output-telemetry-xml "../../backend/src/main/yamcs/mdb/rocket.xml" --output-commanding-xml "../../backend/src/main/yamcs/mdb/commands.xml" ''' or ''' set -e @@ -60,15 +59,14 @@ local_resource( fi ./venv/bin/pip install -r requirements.txt - ./venv/bin/python converter.py --output "../backend/src/main/yamcs/mdb/rocket.xml" + ./venv/bin/python ./src/xtce_generator.py --output-telemetry-xml "../backend/src/main/yamcs/mdb/rocket.xml" --output-commanding-xml "../backend/src/main/yamcs/mdb/commands.xml" ''', deps=[ "requirements.txt", - "converter.py", + "xtce_generator.py", ] ) - local_resource( 'yamcs-effect', serve_cmd="pnpm turbo dev --filter @mrt/yamcs-effect", diff --git a/apps/backend/src/main/java/org/yamcs/mrt/AstraCommandLink.java b/apps/backend/src/main/java/org/yamcs/mrt/AstraCommandLink.java index 26afa59..7f9e9b1 100644 --- a/apps/backend/src/main/java/org/yamcs/mrt/AstraCommandLink.java +++ b/apps/backend/src/main/java/org/yamcs/mrt/AstraCommandLink.java @@ -10,6 +10,7 @@ import org.yamcs.YConfiguration; import org.yamcs.YamcsServer; import org.yamcs.CommandOption.CommandOptionType; +import org.yamcs.cmdhistory.CommandHistoryPublisher; import org.yamcs.cmdhistory.CommandHistoryPublisher.AckStatus; import org.yamcs.commanding.Acknowledgment; import org.yamcs.commanding.ActiveCommand; @@ -61,7 +62,7 @@ public class AstraCommandLink extends AbstractTcDataLink { // time. We store each command and the devices it was sent to. private final AtomicInteger currentCommandId = new AtomicInteger(1); - private Map> commandToDeviceMap = new HashMap<>(); + private Map> commandToDeviceMap = new HashMap<>(); private Map commandToPreparedMap = new HashMap<>(); @Override @@ -201,6 +202,8 @@ public boolean sendCommand(PreparedCommand preparedCommand) { this.commandHistoryPublisher.publish(preparedCommand.getCommandId(), "TX_Devices", String.join(",", devices)); + this.commandToDeviceMap.put(seqNum, devices); + for (var device : devices) { try { client.publish(device + "/commands", msg, null, new IMqttActionListener() { @@ -257,16 +260,33 @@ private void handleMetadata(String deviceName, MqttMessage message) { } - // FIX: Its possible that if there are two radios on the same - // frequency (i.e. pad & cs) then the ack will be sent twice. - public void handleFCAck(int cmd_id, String frequency) { + // FIX: Currently the only way we reigster the FC ack is if the same + // radio that sent it recieved it. Reciving FC acks should be + // radio agnostic, but we don't do that right now + public void handleFCAck(int cmd_id, String frequency, String deviceName) { PreparedCommand command = commandToPreparedMap.get(cmd_id); + Collection devices = commandToDeviceMap.get(cmd_id); + + if (devices.contains(deviceName)) { + commandHistoryPublisher.publishAck( + command.getCommandId(), + "fc_" + frequency, + timeService.getMissionTime(), + AckStatus.OK); + + devices.remove(deviceName); + } + + // If both FCs have ack'd then the command + // is complete + if (devices.size() == 0) { + commandHistoryPublisher.publishAck( + command.getCommandId(), + CommandHistoryPublisher.CommandComplete_KEY, + timeService.getMissionTime(), + AckStatus.OK); + } - commandHistoryPublisher.publishAck( - command.getCommandId(), - "fc_" + frequency, - timeService.getMissionTime(), - AckStatus.OK); } private void handleAck(String deviceName, MqttMessage message) { diff --git a/apps/backend/src/main/java/org/yamcs/mrt/MqttUtils.java b/apps/backend/src/main/java/org/yamcs/mrt/MqttUtils.java index bc79811..9d7e7b3 100644 --- a/apps/backend/src/main/java/org/yamcs/mrt/MqttUtils.java +++ b/apps/backend/src/main/java/org/yamcs/mrt/MqttUtils.java @@ -2,7 +2,6 @@ import java.util.Arrays; import java.util.List; - import org.eclipse.paho.client.mqttv3.IMqttActionListener; import org.eclipse.paho.client.mqttv3.IMqttMessageListener; import org.eclipse.paho.client.mqttv3.IMqttToken; @@ -12,183 +11,205 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.yamcs.ConfigurationException; import org.yamcs.Spec; +import org.yamcs.Spec.OptionType; import org.yamcs.YConfiguration; import org.yamcs.events.EventProducer; import org.yamcs.logging.Log; -import org.yamcs.Spec.OptionType; -/** - * A set of utilities used by the MQTT packet and frame links to avoid code - * duplication - */ +/** A set of utilities used by the MQTT packet and frame links to avoid code duplication */ public class MqttUtils { - /** - * create a new MQTT async client with the clientId and initial broker loaded - * from the config object - */ - static MqttAsyncClient newClient(YConfiguration config) throws ConfigurationException { - try { - List brokers = config.getList("brokers"); - String clientId = config.getString("clientId", MqttClient.generateClientId()); - - return new MqttAsyncClient(brokers.get(0), clientId); - } catch (MqttException e) { - throw new ConfigurationException(e); - } - } - - static MqttConnectOptions getConnectionOptions(YConfiguration config) { - MqttConnectOptions connOpts = new MqttConnectOptions(); - - connOpts.setAutomaticReconnect(true); - List brokers = config.getList("brokers"); - connOpts.setServerURIs(brokers.toArray(new String[0])); - if (config.containsKey("username")) { - connOpts.setUserName(config.getString("username")); - connOpts.setPassword(config.getString("password").toCharArray()); - } - connOpts.setConnectionTimeout(config.getInt("connectionTimeoutSecs")); - connOpts.setKeepAliveInterval(config.getInt("keepAliveSecs")); - connOpts.setCleanSession(true); - - return connOpts; - } - - static void addConnectionOptionsToSpec(Spec spec) { - spec.addOption("brokers", OptionType.LIST).withElementType(OptionType.STRING).withRequired(true); - spec.addOption("username", OptionType.STRING).withRequired(false); - spec.addOption("password", OptionType.STRING).withRequired(false); - spec.addOption("clientId", OptionType.STRING).withRequired(false); - - spec.addOption("connectionTimeoutSecs", OptionType.INTEGER).withDefault(5); - spec.addOption("autoReconnect", OptionType.BOOLEAN).withRequired(false).withDefault(true); - spec.addOption("keepAliveSecs", OptionType.INTEGER).withDefault(60); - spec.requireTogether("username", "password"); - } - - /** - * Connect to MQTT - */ - static void connect(MqttConnectOptions connOpts, MqttAsyncClient client, Log log, EventProducer eventProducer) - throws MqttException { - log.info("Connecting to MQTT with clientId {} and options: {}", client.getClientId(), connOpts); - - client.connect(connOpts, null, new IMqttActionListener() { - @Override - public void onSuccess(IMqttToken token) { - log.info("Succesfully connected to MQTT"); - - } - - @Override - public void onFailure(IMqttToken t, Throwable e) { - String msg = "Failed to connect to MQTT with clientId " + client.getClientId() + ": " + e.getMessage(); - eventProducer.sendWarning(msg); - log.warn("{}", msg); - } - }); - } - - /** - * Connect MQTT and subscribe to a given topic - */ - static void connectAndSubscribe(MqttConnectOptions connOpts, MqttAsyncClient client, - IMqttMessageListener messageListener, String topic, Log log, EventProducer eventProducer, - SubscriptionFailureCallback subscriptionFailureCallback) - throws MqttException { - log.info("Connecting to MQTT with clientId {} and options: {}", client.getClientId(), connOpts); - - client.connect(connOpts, null, new IMqttActionListener() { - @Override - public void onSuccess(IMqttToken token) { - log.info("Succesfully connected to MQTT"); - try { - client.subscribe(topic, 2, messageListener).setActionCallback(new IMqttActionListener() { - @Override - public void onSuccess(IMqttToken t) { - int[] granted = t.getGrantedQos(); - if (granted.length != 1 || granted[0] > 2) { - String msg = "Subscription to " + topic + " failed; granted QoS: " - + Arrays.asList(granted); - eventProducer.sendWarning(msg); - subscriptionFailureCallback.setSubscriptionFailure(new Exception(msg)); - } else { - log.info("Succesfully subscribed to {}", topic); - } - } - - @Override - public void onFailure(IMqttToken t, Throwable e) { - String msg = "Subscription to " + topic + " failed: " + e.getMessage(); - eventProducer.sendWarning(msg); - log.warn("{}", msg); - subscriptionFailureCallback.setSubscriptionFailure(e); - } - - }); - } catch (MqttException e) { - subscriptionFailureCallback.setSubscriptionFailure(e); - } - } - - @Override - public void onFailure(IMqttToken t, Throwable e) { - String msg = "Failed to connect to MQTT with clientId " + client.getClientId() + ": " + e.getMessage(); - eventProducer.sendWarning(msg); - log.warn("{}", msg); - } - }); - } - - public static void doDisable(MqttAsyncClient client) throws MqttException { - if (client.isConnected()) { - client.disconnect(); - } - } - - public static void doStop(MqttAsyncClient client, NotifyStoppedCallback stopCb, NotifyFailedCallback failCb) { - try { - if (client.isConnected()) { - client.disconnect(null, - new IMqttActionListener() { - @Override - public void onSuccess(IMqttToken t) { - try { - client.close(); - stopCb.notifyStopped(); - } catch (MqttException e) { - failCb.notifyFailed(e); - } - } - - @Override - public void onFailure(IMqttToken t, Throwable e) { - failCb.notifyFailed(e); - } - }); - } else { - client.disconnectForcibly(0, 0, false); - client.close(); - stopCb.notifyStopped(); - } - } catch (MqttException e) { - failCb.notifyFailed(e); - } - } - - @FunctionalInterface - public interface NotifyStoppedCallback { - void notifyStopped(); - } - - @FunctionalInterface - public interface NotifyFailedCallback { - void notifyFailed(Throwable e); - } - - @FunctionalInterface - public interface SubscriptionFailureCallback { - void setSubscriptionFailure(Throwable e); - } + /** + * create a new MQTT async client with the clientId and initial broker loaded from the config + * object + */ + static MqttAsyncClient newClient(YConfiguration config) throws ConfigurationException { + try { + List brokers = config.getList("brokers"); + String clientId = config.getString("clientId", MqttClient.generateClientId()); + + return new MqttAsyncClient(brokers.get(0), clientId); + } catch (MqttException e) { + throw new ConfigurationException(e); + } + } + + static MqttConnectOptions getConnectionOptions(YConfiguration config) { + MqttConnectOptions connOpts = new MqttConnectOptions(); + + connOpts.setAutomaticReconnect(true); + List brokers = config.getList("brokers"); + connOpts.setServerURIs(brokers.toArray(new String[0])); + if (config.containsKey("username")) { + connOpts.setUserName(config.getString("username")); + connOpts.setPassword(config.getString("password").toCharArray()); + } + connOpts.setConnectionTimeout(config.getInt("connectionTimeoutSecs")); + connOpts.setKeepAliveInterval(config.getInt("keepAliveSecs")); + connOpts.setCleanSession(true); + + return connOpts; + } + + static void addConnectionOptionsToSpec(Spec spec) { + spec.addOption("brokers", OptionType.LIST) + .withElementType(OptionType.STRING) + .withRequired(true); + spec.addOption("username", OptionType.STRING).withRequired(false); + spec.addOption("password", OptionType.STRING).withRequired(false); + spec.addOption("clientId", OptionType.STRING).withRequired(false); + + spec.addOption("connectionTimeoutSecs", OptionType.INTEGER).withDefault(5); + spec.addOption("autoReconnect", OptionType.BOOLEAN).withRequired(false).withDefault(true); + spec.addOption("keepAliveSecs", OptionType.INTEGER).withDefault(60); + spec.requireTogether("username", "password"); + } + + /** Connect to MQTT */ + static void connect( + MqttConnectOptions connOpts, MqttAsyncClient client, Log log, EventProducer eventProducer) + throws MqttException { + // System.out.println( + // "Connecting to MQTT with clientId {} and options: {}", client.getClientId(), connOpts); + + client.connect( + connOpts, + null, + new IMqttActionListener() { + @Override + public void onSuccess(IMqttToken token) { + log.info("Succesfully connected to MQTT"); + } + + @Override + public void onFailure(IMqttToken t, Throwable e) { + String msg = + "Failed to connect to MQTT with clientId " + + client.getClientId() + + ": " + + e.getMessage(); + eventProducer.sendWarning(msg); + log.warn("{}", msg); + } + }); + } + + /** Connect MQTT and subscribe to a given topic */ + static void connectAndSubscribe( + MqttConnectOptions connOpts, + MqttAsyncClient client, + IMqttMessageListener messageListener, + String topic, + Log log, + EventProducer eventProducer, + SubscriptionFailureCallback subscriptionFailureCallback) + throws MqttException { + log.info("Connecting to MQTT with clientId {} and options: {}", client.getClientId(), connOpts); + + client.connect( + connOpts, + null, + new IMqttActionListener() { + @Override + public void onSuccess(IMqttToken token) { + log.info("Succesfully connected to MQTT"); + try { + client + .subscribe(topic, 2, messageListener) + .setActionCallback( + new IMqttActionListener() { + @Override + public void onSuccess(IMqttToken t) { + int[] granted = t.getGrantedQos(); + if (granted.length != 1 || granted[0] > 2) { + String msg = + "Subscription to " + + topic + + " failed; granted QoS: " + + Arrays.asList(granted); + eventProducer.sendWarning(msg); + subscriptionFailureCallback.setSubscriptionFailure(new Exception(msg)); + } else { + log.info("Succesfully subscribed to {}", topic); + } + } + + @Override + public void onFailure(IMqttToken t, Throwable e) { + String msg = "Subscription to " + topic + " failed: " + e.getMessage(); + eventProducer.sendWarning(msg); + log.warn("{}", msg); + subscriptionFailureCallback.setSubscriptionFailure(e); + } + }); + } catch (MqttException e) { + subscriptionFailureCallback.setSubscriptionFailure(e); + } + } + + @Override + public void onFailure(IMqttToken t, Throwable e) { + String msg = + "Failed to connect to MQTT with clientId " + + client.getClientId() + + ": " + + e.getMessage(); + eventProducer.sendWarning(msg); + log.warn("{}", msg); + } + }); + } + + public static void doDisable(MqttAsyncClient client) throws MqttException { + if (client.isConnected()) { + client.disconnect(); + } + } + + public static void doStop( + MqttAsyncClient client, NotifyStoppedCallback stopCb, NotifyFailedCallback failCb) { + try { + if (client.isConnected()) { + client.disconnect( + null, + new IMqttActionListener() { + @Override + public void onSuccess(IMqttToken t) { + try { + client.close(); + stopCb.notifyStopped(); + } catch (MqttException e) { + failCb.notifyFailed(e); + } + } + + @Override + public void onFailure(IMqttToken t, Throwable e) { + failCb.notifyFailed(e); + } + }); + } else { + client.disconnectForcibly(0, 0, false); + client.close(); + stopCb.notifyStopped(); + } + } catch (MqttException e) { + failCb.notifyFailed(e); + } + } + + @FunctionalInterface + public interface NotifyStoppedCallback { + void notifyStopped(); + } + + @FunctionalInterface + public interface NotifyFailedCallback { + void notifyFailed(Throwable e); + } + + @FunctionalInterface + public interface SubscriptionFailureCallback { + void setSubscriptionFailure(Throwable e); + } } diff --git a/apps/backend/src/main/java/org/yamcs/mrt/astra/AstraSubLink.java b/apps/backend/src/main/java/org/yamcs/mrt/astra/AstraSubLink.java index cde501e..0fad298 100644 --- a/apps/backend/src/main/java/org/yamcs/mrt/astra/AstraSubLink.java +++ b/apps/backend/src/main/java/org/yamcs/mrt/astra/AstraSubLink.java @@ -2,7 +2,6 @@ import java.util.HashMap; import java.util.Map; - import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.yamcs.ConfigurationException; @@ -10,72 +9,73 @@ import org.yamcs.tctm.AbstractTmDataLink; public abstract class AstraSubLink extends AbstractTmDataLink { - private Status status = Status.UNAVAIL; - private String detailedStatus = ""; - MqttAsyncClient client; - - public AstraSubLink(MqttAsyncClient client) { - this.client = client; - } - - @Override - public void init(String yamcsInstance, String linkName, YConfiguration config) - throws ConfigurationException { - Map cfgMap = config.getRoot(); - Map args = new HashMap<>(); - - args.put("timestampOffset", -1); - args.put("seqCountOffset", 0); - - cfgMap.put("packetPreprocessorClassName", "org.yamcs.tctm.GenericPacketPreprocessor"); - cfgMap.put("packetPreprocessorArgs", args); - - YConfiguration cfg = YConfiguration.wrap(cfgMap); - - super.init(yamcsInstance, linkName, cfg); - } - - @Override - public String getDetailedStatus() { - return detailedStatus; - } - - public void setDetailedStatus(String detailedStatus) { - this.detailedStatus = detailedStatus; - } - - public void setStatus(String statusString) { - Status status = switch (statusString) { - case "OK" -> Status.OK; - case "UNAVAIL" -> Status.UNAVAIL; - case "FAILED" -> Status.FAILED; - case "DISABLED" -> Status.DISABLED; - default -> throw new IllegalArgumentException("Unknown status type: " + statusString); - }; - - this.status = status; - } - - public abstract void handleMqttMessage(MqttMessage message); - - @Override - protected Status connectionStatus() { - return status; - } - - @Override - protected void doStart() { - notifyStarted(); - } - - @Override - protected void doDisable() throws Exception { - this.detailedStatus = "Device disconnected or stopped."; - super.doDisable(); - } - - @Override - protected void doStop() { - notifyStopped(); - } + private Status status = Status.UNAVAIL; + private String detailedStatus = ""; + MqttAsyncClient client; + + public AstraSubLink(MqttAsyncClient client) { + this.client = client; + } + + @Override + public void init(String yamcsInstance, String linkName, YConfiguration config) + throws ConfigurationException { + Map cfgMap = config.getRoot(); + Map args = new HashMap<>(); + + args.put("timestampOffset", -1); + args.put("seqCountOffset", 0); + + cfgMap.put("packetPreprocessorClassName", "org.yamcs.tctm.GenericPacketPreprocessor"); + cfgMap.put("packetPreprocessorArgs", args); + + YConfiguration cfg = YConfiguration.wrap(cfgMap); + + super.init(yamcsInstance, linkName, cfg); + } + + @Override + public String getDetailedStatus() { + return detailedStatus; + } + + public void setDetailedStatus(String detailedStatus) { + this.detailedStatus = detailedStatus; + } + + public void setStatus(String statusString) { + Status status = + switch (statusString) { + case "OK" -> Status.OK; + case "UNAVAIL" -> Status.UNAVAIL; + case "FAILED" -> Status.FAILED; + case "DISABLED" -> Status.DISABLED; + default -> throw new IllegalArgumentException("Unknown status type: " + statusString); + }; + + this.status = status; + } + + public abstract void handleMqttMessage(MqttMessage message); + + @Override + protected Status connectionStatus() { + return status; + } + + @Override + protected void doStart() { + notifyStarted(); + } + + @Override + protected void doDisable() throws Exception { + this.detailedStatus = "Device disconnected or stopped."; + super.doDisable(); + } + + @Override + protected void doStop() { + notifyStopped(); + } } diff --git a/apps/backend/src/main/java/org/yamcs/mrt/astra/RadiosLink.java b/apps/backend/src/main/java/org/yamcs/mrt/astra/RadiosLink.java index aa0c80a..dc0facc 100644 --- a/apps/backend/src/main/java/org/yamcs/mrt/astra/RadiosLink.java +++ b/apps/backend/src/main/java/org/yamcs/mrt/astra/RadiosLink.java @@ -1,181 +1,66 @@ package org.yamcs.mrt.astra; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.yamcs.ConfigurationException; import org.yamcs.YConfiguration; import org.yamcs.YamcsServer; -import org.yamcs.client.Acknowledgment; -import org.yamcs.cmdhistory.Attribute; -import org.yamcs.cmdhistory.CommandHistoryPublisher; -import org.yamcs.cmdhistory.CommandHistoryPublisher.AckStatus; -import org.yamcs.commanding.PreparedCommand; import org.yamcs.mrt.AstraCommandLink; import org.yamcs.mrt.DefaultMqttToTmPacketConverter; import org.yamcs.mrt.MqttToTmPacketConverter; -import org.yamcs.parameter.UInt32Value; -import org.yamcs.protobuf.YamcsInstance; import org.yamcs.tctm.Link; -import org.yamcs.protobuf.Commanding.CommandId; public class RadiosLink extends AstraSubLink { - MqttToTmPacketConverter tmConverter; - private String deviceName; - private String deviceFrequency; - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - - public RadiosLink(MqttAsyncClient client, String frequency) { - super(client); - this.deviceFrequency = frequency; - } - - @Override - public void init(String yamcsInstance, String linkName, YConfiguration config) - throws ConfigurationException { - super.init(yamcsInstance, linkName, config); - - this.deviceName = linkName.split("/")[1]; - - tmConverter = new DefaultMqttToTmPacketConverter(); - tmConverter.init(yamcsInstance, linkName, config); - } + MqttToTmPacketConverter tmConverter; + private String deviceName; + private String deviceFrequency; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - // @Override - // public boolean sendCommand(PreparedCommand preparedCommand) { - // String cmdId = preparedCommand.getMetaCommand().getShortDescription(); - // - // int seqNum = commandCountCounter.getAndIncrement(); - // if (seqNum > 255) { - // commandCountCounter.set(1); - // seqNum = 1; - // } - // - // this.commandHistoryPublisher.publish(preparedCommand.getCommandId(), - // "Command_Id", cmdId); - // this.commandHistoryPublisher.publish(preparedCommand.getCommandId(), - // "Sequence_Count", seqNum); - // - // pendingCommands.put(seqNum, preparedCommand); - // - // String cmdPayload = seqNum + "," + cmdId; - // - // MqttMessage msg = new MqttMessage(cmdPayload.getBytes()); - // - // try { - // client.publish(this.deviceName + "/commands", msg); - // - // long missionTime = this.timeService.getMissionTime(); - // - // this.commandHistoryPublisher.publishAck( - // preparedCommand.getCommandId(), - // Acknowledgment.SENT, - // missionTime, - // AckStatus.OK); - // - // this.commandHistoryPublisher.publishAck( - // preparedCommand.getCommandId(), - // "Acknowledge_Radio_RX", - // missionTime, - // AckStatus.PENDING); - // - // } catch (MqttException e) { - // eventProducer.sendDistress(e.getLocalizedMessage()); - // e.printStackTrace(); - // return false; - // } - // - // return true; - // } - // - // @Override - // public void handleAck(Number cmdId, String status) { - // System.out.println("GOT ACK FOR cmdId: " + cmdId + " status: " + status); - // - // String ackType = status.substring(0, 2); - // long missionTime = this.timeService.getMissionTime(); - // - // PreparedCommand preparedCommand = pendingCommands.get(cmdId.intValue()); - // if (preparedCommand != null) { - // AckStatus ackStatus; - // if (status.contains("OK")) { - // ackStatus = AckStatus.OK; - // } else if (status.contains("BAD")) { - // ackStatus = AckStatus.NOK; - // } else { - // ackStatus = AckStatus.CANCELLED; - // } - // - // this.commandHistoryPublisher.publishAck( - // preparedCommand.getCommandId(), - // "Acknowledge_Radio_" + ackType, - // missionTime, - // ackStatus); - // - // if (ackType == "RX" && ackStatus == AckStatus.OK) { - // this.commandHistoryPublisher.publishAck( - // preparedCommand.getCommandId(), - // "Acknowledge_Radio_TX", - // missionTime, - // AckStatus.PENDING); - // } - // } else { - // System.out.println("No pending command found for cmdId: " + cmdId); - // } - // } - // - // private void handleFCAck(int commandAckId) { - // PreparedCommand command = pendingCommands.get(commandAckId); - // if (command == null) { - // eventProducer.sendCritical( - // "Recieved ACK for id \"" + commandAckId + "\" but no such command was found - // in the ground station."); - // } - // - // this.commandHistoryPublisher.publishAck( - // command.getCommandId(), - // CommandHistoryPublisher.CommandComplete_KEY, - // this.timeService.getMissionTime(), - // AckStatus.OK); - // } + public RadiosLink(MqttAsyncClient client, String frequency) { + super(client); + this.deviceFrequency = frequency; + } - @Override - public void handleMqttMessage(MqttMessage message) { - dataIn(1, message.getPayload().length); + @Override + public void init(String yamcsInstance, String linkName, YConfiguration config) + throws ConfigurationException { + super.init(yamcsInstance, linkName, config); - // my friend encoded this in cpp, and for him - // he set the flag at bit index #1 (2nd from the right) - // can you extract it as a boolean - byte flags = message.getPayload()[2]; - boolean ackFlag = ((flags >> 1) & 1) == 1; - if (ackFlag) { - byte commandAckIdByte = message.getPayload()[3]; - int commandAckId = commandAckIdByte & 0xFF; + this.deviceName = linkName.split("/")[1]; - var links = YamcsServer.getServer().getInstance(this.getYamcsInstance()).getLinkManager().getLinks(); - for (Link link : links) { - if (link instanceof AstraCommandLink) { - ((AstraCommandLink) link).handleFCAck(commandAckId, deviceFrequency); - } - } - } + tmConverter = new DefaultMqttToTmPacketConverter(); + tmConverter.init(yamcsInstance, linkName, config); + } - for (var tmPacket : tmConverter.convert(message)) { + @Override + public void handleMqttMessage(MqttMessage message) { + dataIn(1, message.getPayload().length); - tmPacket = packetPreprocessor.process(tmPacket); - if (tmPacket != null) { - super.processPacket(tmPacket); - } - } + // This is the only point where we can access the binary packet + // In order to pass acks along to toe AstraCommandLinkClass + byte flags = message.getPayload()[2]; + boolean ackFlag = ((flags >> 1) & 1) == 1; + if (ackFlag) { + byte commandAckIdByte = message.getPayload()[3]; + int commandAckId = commandAckIdByte & 0xFF; - } + var links = + YamcsServer.getServer().getInstance(this.getYamcsInstance()).getLinkManager().getLinks(); + for (Link link : links) { + if (link instanceof AstraCommandLink) { + ((AstraCommandLink) link).handleFCAck(commandAckId, deviceFrequency, deviceName); + } + } + } + dataIn(1, message.getPayload().length); + for (var tmPacket : tmConverter.convert(message)) { + tmPacket = packetPreprocessor.process(tmPacket); + if (tmPacket != null) { + super.processPacket(tmPacket); + } + } + } } diff --git a/apps/backend/src/main/java/org/yamcs/mrt/links/AstraGenericTmLink.java b/apps/backend/src/main/java/org/yamcs/mrt/links/AstraGenericTmLink.java new file mode 100644 index 0000000..0980773 --- /dev/null +++ b/apps/backend/src/main/java/org/yamcs/mrt/links/AstraGenericTmLink.java @@ -0,0 +1,105 @@ +package org.yamcs.mrt.links; + +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.yamcs.YConfiguration; +import org.yamcs.logging.Log; +import org.yamcs.mrt.DefaultMqttToTmPacketConverter; +import org.yamcs.mrt.MqttToTmPacketConverter; +import org.yamcs.mrt.utils.MqttManager; +import org.yamcs.mrt.utils.MqttTopicHandler; +import org.yamcs.tctm.AbstractTmDataLink; + +public class AstraGenericTmLink extends AbstractTmDataLink implements MqttTopicHandler { + MqttToTmPacketConverter tmConverter; + + // Local State + private String baseTopic; + private long dataInCount; + + private Status status = Status.UNAVAIL; + private String detailedStatus = ""; + private static final Log log = new Log(AstraGenericTmLink.class); + + @Override + public void init(String instance, String name, YConfiguration config) { + MqttManager manager = MqttManager.getInstance(); + this.baseTopic = name; + + tmConverter = new DefaultMqttToTmPacketConverter(); + tmConverter.init(yamcsInstance, linkName, config); + + try { + manager.subscribe(baseTopic + "/telemetry", this); + manager.subscribe(baseTopic + "/status", this); + manager.subscribe(baseTopic + "/detail", this); + } catch (MqttException e) { + e.printStackTrace(); + } + + super.init(instance, name, config); + } + + @Override + public void doStart() { + notifyStarted(); + } + + @Override + public void doStop() { + notifyStopped(); + } + + @Override + public Status connectionStatus() { + return this.status; + } + + @Override + public Status getLinkStatus() { + return this.status; + } + + @Override + public String getDetailedStatus() { + return this.detailedStatus; + } + + @Override + public long getDataInCount() { + return this.dataInCount; + } + + @Override + public void handleMqtt(String topic, MqttMessage message) { + + if (topic.equals(baseTopic + "/telemetry")) { + dataInCount += message.getPayload().length; + + for (var tmPacket : tmConverter.convert(message)) { + tmPacket = packetPreprocessor.process(tmPacket); + + if (tmPacket != null) { + processPacket(tmPacket); + } + } + + } else if (topic.equals(baseTopic + "/detail")) { + String payload = new String(message.getPayload()); + this.detailedStatus = payload; + } else if (topic.equals(baseTopic + "/status")) { + String payload = new String(message.getPayload()); + Status newStatus = + switch (payload) { + case "OK" -> Status.OK; + case "FAILED" -> Status.FAILED; + case "DISABLED" -> Status.DISABLED; + case "UNAVAIL" -> Status.UNAVAIL; + default -> Status.UNAVAIL; + }; + + log.info(topic + " " + new String(message.getPayload()) + " " + newStatus); + this.status = newStatus; + } + } +} diff --git a/apps/backend/src/main/java/org/yamcs/mrt/links/ControlBoxLink.java b/apps/backend/src/main/java/org/yamcs/mrt/links/ControlBoxLink.java new file mode 100644 index 0000000..1ed5a70 --- /dev/null +++ b/apps/backend/src/main/java/org/yamcs/mrt/links/ControlBoxLink.java @@ -0,0 +1,324 @@ +package org.yamcs.mrt.links; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.Set; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.yamcs.YConfiguration; +import org.yamcs.labjack.LabJackDataLink; +import org.yamcs.logging.Log; +import org.yamcs.mrt.DefaultMqttToTmPacketConverter; +import org.yamcs.mrt.MqttToTmPacketConverter; +import org.yamcs.mrt.utils.MqttManager; +import org.yamcs.mrt.utils.MqttTopicHandler; +import org.yamcs.tctm.AbstractTmDataLink; + +public class ControlBoxLink extends AbstractTmDataLink implements MqttTopicHandler { + MqttToTmPacketConverter tmConverter; + + // Local State + private String baseTopic; + private long dataInCount; + + private Status status = Status.UNAVAIL; + private String detailedStatus = ""; + private static final Log log = new Log(AstraGenericTmLink.class); + + // Previous switch states for change detection (null = no previous packet received yet) + private byte[] previousSwitchStates = null; + + private static final int YAMCS_HTTP_PORT = 8090; + private static final String YAMCS_INSTANCE = "ground_station"; + private static final String YAMCS_PROCESSOR = "realtime"; + + private final HttpClient httpClient = HttpClient.newHttpClient(); + + /** + * Switch-to-LabJack pin mapping. Each entry maps a byte offset in the ControlBox telemetry packet + * to the corresponding LabJack digital pin number. + * + *

Packet layout (from controlbox.xml): byte 0: panel_1_switch_estop byte 1: + * panel_2_switch_launch byte 2: panel_3_switch_1 byte 3: panel_3_switch_2 byte 4: + * panel_4_switch_1 byte 5: panel_4_switch_2 byte 6: panel_5_switch_1 byte 7: panel_5_switch_2 + * byte 8: panel_6_switch_1 byte 9: panel_6_switch_2 byte 10: panel_7_switch_1 byte 11: + * panel_7_switch_2 byte 12: panel_8_switch_1 byte 13: panel_8_switch_2 byte 14: + * panel_9_switch_key + * + *

Pin number -1 means the switch is not mapped to a LabJack pin. + * + *

When a switch changes, issueWriteDigitalPinCommand() sends an async HTTP POST to + * http://localhost:8090/api/processors/ground_station/realtime/commands/LabJackT7/write_digital_pin + * with pin_number and pin_state (HIGH/LOW). + */ + private static final Map SWITCH_PIN_MAP = + Map.ofEntries( + Map.entry(0, new SwitchMapping("panel_1_switch_estop", -1)), // E-stop: not mapped + Map.entry(1, new SwitchMapping("panel_2_switch_launch", -1)), // Launch: not mapped + Map.entry(2, new SwitchMapping("panel_3_switch_1", 0)), // FIO0 + Map.entry(3, new SwitchMapping("panel_3_switch_2", 1)), // FIO1 + Map.entry(4, new SwitchMapping("panel_4_switch_1", 2)), // FIO2 + Map.entry(5, new SwitchMapping("panel_4_switch_2", 3)), // FIO3 + Map.entry(6, new SwitchMapping("panel_5_switch_1", 4)), // FIO4 + Map.entry(7, new SwitchMapping("panel_5_switch_2", 5)), // FIO5 + Map.entry(8, new SwitchMapping("panel_6_switch_1", 6)), // FIO6 + Map.entry(9, new SwitchMapping("panel_6_switch_2", 7)), // FIO7 + Map.entry(10, new SwitchMapping("panel_7_switch_1", 8)), // EIO0 + Map.entry(11, new SwitchMapping("panel_7_switch_2", 9)), // EIO1 + Map.entry(12, new SwitchMapping("panel_8_switch_1", 10)), // EIO2 + Map.entry(13, new SwitchMapping("panel_8_switch_2", 11)), // EIO3 + Map.entry(14, new SwitchMapping("panel_9_switch_key", -1)) // Key: not mapped + ); + + private record SwitchMapping(String name, int labJackPin) {} + + /** Byte offset of the arming key switch in the telemetry packet. */ + private static final int ARMING_KEY_OFFSET = 14; + + /** + * Switches that require the arming key to be ON before their commands are dispatched. Identified + * by their byte offset in the telemetry packet. If a switch in this set changes while the key is + * OFF, the command is blocked and a warning is logged. + */ + private static final Set ARMING_KEY_GUARDED_SWITCHES = + Set.of( + 12, // panel_8_switch_1 + 13 // panel_8_switch_2 + ); + + /** Byte offset of the emergency stop switch in the telemetry packet. */ + private static final int ESTOP_OFFSET = 0; + + @Override + public void init(String instance, String name, YConfiguration config) { + MqttManager manager = MqttManager.getInstance(); + this.baseTopic = name; + + tmConverter = new DefaultMqttToTmPacketConverter(); + tmConverter.init(yamcsInstance, linkName, config); + + try { + manager.subscribe(baseTopic + "/telemetry", this); + manager.subscribe(baseTopic + "/status", this); + manager.subscribe(baseTopic + "/detail", this); + } catch (MqttException e) { + e.printStackTrace(); + } + + super.init(instance, name, config); + } + + @Override + public void doStart() { + notifyStarted(); + } + + @Override + public void doStop() { + notifyStopped(); + } + + @Override + public Status connectionStatus() { + return this.status; + } + + @Override + public Status getLinkStatus() { + return this.status; + } + + @Override + public String getDetailedStatus() { + return this.detailedStatus; + } + + @Override + public long getDataInCount() { + return this.dataInCount; + } + + @Override + public void handleMqtt(String topic, MqttMessage message) { + + if (topic.equals(baseTopic + "/telemetry")) { + dataInCount += message.getPayload().length; + + byte[] payload = message.getPayload(); + detectAndDispatchChanges(payload); + + for (var tmPacket : tmConverter.convert(message)) { + tmPacket = packetPreprocessor.process(tmPacket); + + if (tmPacket != null) { + processPacket(tmPacket); + } + } + + } else if (topic.equals(baseTopic + "/detail")) { + String payload = new String(message.getPayload()); + this.detailedStatus = payload; + } else if (topic.equals(baseTopic + "/status")) { + String payload = new String(message.getPayload()); + Status newStatus = + switch (payload) { + case "OK" -> Status.OK; + case "FAILED" -> Status.FAILED; + case "DISABLED" -> Status.DISABLED; + case "UNAVAIL" -> Status.UNAVAIL; + default -> Status.UNAVAIL; + }; + + log.info(topic + " " + new String(message.getPayload()) + " " + newStatus); + this.status = newStatus; + } + } + + /** + * Compares the current telemetry packet against the previous one to detect switch state changes. + * For each changed switch that has a LabJack pin mapping, issues a /LabJackT7/write_digital_pin + * command via the Yamcs HTTP API. + */ + private void detectAndDispatchChanges(byte[] currentPayload) { + if (previousSwitchStates == null) { + // First packet received, store as baseline + previousSwitchStates = currentPayload.clone(); + return; + } + + int numSwitches = Math.min(currentPayload.length, previousSwitchStates.length); + boolean armingKeyOn = + currentPayload.length > ARMING_KEY_OFFSET && currentPayload[ARMING_KEY_OFFSET] != 0; + boolean estopOn = currentPayload.length > ESTOP_OFFSET && currentPayload[ESTOP_OFFSET] != 0; + + // E-stop activation: on transition to ON, immediately set all mapped pins LOW + if (numSwitches > ESTOP_OFFSET + && currentPayload[ESTOP_OFFSET] != previousSwitchStates[ESTOP_OFFSET] + && estopOn) { + handleEmergencyStop(); + // Store current state as baseline and do not process other switches for this packet + previousSwitchStates = currentPayload.clone(); + return; + } + + // If E-stop is currently asserted, block any other switch actions. Log attempts. + if (estopOn) { + for (int i = 0; i < numSwitches; i++) { + if (i == ESTOP_OFFSET) continue; + if (currentPayload[i] != previousSwitchStates[i]) { + SwitchMapping mapping = SWITCH_PIN_MAP.get(i); + String name = mapping != null ? mapping.name() : ("switch_" + i); + log.warn("Blocked switch change for " + name + " because E-STOP is active"); + } + } + // Keep baseline in sync so changes made while E-stop was active are ignored + previousSwitchStates = currentPayload.clone(); + return; + } + + // Normal processing when no E-stop active + for (int i = 0; i < numSwitches; i++) { + if (currentPayload[i] != previousSwitchStates[i]) { + SwitchMapping mapping = SWITCH_PIN_MAP.get(i); + if (mapping == null) { + continue; + } + + boolean newState = currentPayload[i] != 0; + log.info("Switch state change: " + mapping.name() + " -> " + (newState ? "ON" : "OFF")); + + if (ARMING_KEY_GUARDED_SWITCHES.contains(i) && !armingKeyOn) { + log.warn("Blocked command for " + mapping.name() + ": arming key is OFF"); + continue; + } + + if (mapping.labJackPin() >= 0) { + issueWriteDigitalPinCommand(mapping.labJackPin(), newState, mapping.name()); + } + } + } + + previousSwitchStates = currentPayload.clone(); + } + + /** + * Handles emergency stop activation by immediately setting all mapped LabJack pins to LOW. Uses + * direct LabJackDataLink calls (bypassing the HTTP API) for minimal latency. + */ + private void handleEmergencyStop() { + log.warn("EMERGENCY STOP ACTIVATED - setting all mapped pins to LOW"); + + LabJackDataLink labJack = LabJackDataLink.getInstance(); + if (labJack == null) { + log.error("E-stop: LabJackDataLink instance not available"); + return; + } + + for (var entry : SWITCH_PIN_MAP.values()) { + if (entry.labJackPin() >= 0) { + labJack.writeDigitalPin(entry.labJackPin(), 0); + } + } + + log.warn("EMERGENCY STOP: all mapped pins set to LOW"); + } + + /** + * Issues a /LabJackT7/write_digital_pin command via the Yamcs HTTP API. + * + * @param pinNumber the LabJack digital pin number (0-22) + * @param pinState true for HIGH, false for LOW + * @param switchName the name of the control box switch (for logging) + */ + private void issueWriteDigitalPinCommand(int pinNumber, boolean pinState, String switchName) { + String pinStateStr = pinState ? "HIGH" : "LOW"; + String url = + String.format( + "http://localhost:%d/api/processors/%s/%s/commands/LabJackT7/write_digital_pin", + YAMCS_HTTP_PORT, YAMCS_INSTANCE, YAMCS_PROCESSOR); + + String jsonBody = + String.format( + "{\"args\": {\"pin_number\": %d, \"pin_state\": \"%s\"}}", pinNumber, pinStateStr); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + + httpClient + .sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenAccept( + response -> { + if (response.statusCode() == 200) { + log.info( + "Issued write_digital_pin: pin=" + + pinNumber + + " state=" + + pinStateStr + + " (triggered by " + + switchName + + ")"); + } else { + log.warn( + "Failed to issue write_digital_pin for " + + switchName + + ": HTTP " + + response.statusCode() + + " - " + + response.body()); + } + }) + .exceptionally( + ex -> { + log.error( + "Error issuing write_digital_pin for " + switchName + ": " + ex.getMessage()); + return null; + }); + } +} diff --git a/apps/backend/src/main/java/org/yamcs/mrt/utils/AstraPacketPreprocessor.java b/apps/backend/src/main/java/org/yamcs/mrt/utils/AstraPacketPreprocessor.java new file mode 100644 index 0000000..1c36481 --- /dev/null +++ b/apps/backend/src/main/java/org/yamcs/mrt/utils/AstraPacketPreprocessor.java @@ -0,0 +1,67 @@ +package org.yamcs.mrt.utils; + +import org.yamcs.ConfigurationException; +import org.yamcs.TmPacket; +import org.yamcs.YConfiguration; +import org.yamcs.mdb.MdbFactory; +import org.yamcs.tctm.AbstractPacketPreprocessor; +import org.yamcs.utils.TimeEncoding; +import org.yamcs.xtce.SequenceContainer; + +public class AstraPacketPreprocessor extends AbstractPacketPreprocessor { + + // where from the packet to read the 4 bytes sequence count + final int seqCountOffset = 0; + + // Optional. If unset Yamcs will attempt to determine it in other ways + SequenceContainer rootContainer; + + public AstraPacketPreprocessor(String yamcsInstance, YConfiguration config) { + super(yamcsInstance, config); + + var rootContainerName = config.getString("rootContainer", null); + if (rootContainerName != null) { + var mdb = MdbFactory.getInstance(yamcsInstance); + rootContainer = mdb.getSequenceContainer(rootContainerName); + if (rootContainer == null) { + throw new ConfigurationException( + "MDB does not have a sequence container named '" + rootContainerName + "'"); + } + } + } + + @Override + public TmPacket process(TmPacket tmPacket) { + byte[] packet = tmPacket.getPacket(); + + int seqCount = 0; + if (seqCountOffset >= 0) { + if (packet.length < seqCountOffset + 2) { + eventProducer.sendWarning( + ETYPE_CORRUPTED_PACKET, "Packet too short to extract sequence count"); + seqCount = -1; + } else { + seqCount = getLittleEndianInt16(packet); + } + } + + tmPacket.setGenerationTime(TimeEncoding.getWallclockTime()); + + tmPacket.setSequenceCount(seqCount); + tmPacket.setRootContainer(rootContainer); + return tmPacket; + } + + private int getLittleEndianInt16(byte[] data) { + if (data == null || data.length < 2) { + throw new IllegalArgumentException("Byte array must have at least 2 bytes"); + } + + // & 0xFF converts signed byte to unsigned int + int low = data[0] & 0xFF; + int high = data[1] & 0xFF; + + // Shift high byte 8 bits to the left and combine with low byte + return (high << 8) | low; + } +} diff --git a/apps/backend/src/main/java/org/yamcs/mrt/utils/MqttManager.java b/apps/backend/src/main/java/org/yamcs/mrt/utils/MqttManager.java new file mode 100644 index 0000000..5e7b480 --- /dev/null +++ b/apps/backend/src/main/java/org/yamcs/mrt/utils/MqttManager.java @@ -0,0 +1,52 @@ +package org.yamcs.mrt.utils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.eclipse.paho.client.mqttv3.*; + +public class MqttManager implements MqttCallback { + private static MqttManager instance; + private MqttClient client; + private final String brokerUrl = "tcp://localhost:1883"; + private final Map handlers = new ConcurrentHashMap<>(); + + private MqttManager() { + try { + client = new MqttClient(brokerUrl, MqttClient.generateClientId()); + client.setCallback(this); + MqttConnectOptions options = new MqttConnectOptions(); + options.setCleanSession(true); + client.connect(options); + } catch (MqttException e) { + e.printStackTrace(); + } + } + + public static synchronized MqttManager getInstance() { + if (instance == null) { + instance = new MqttManager(); + } + return instance; + } + + public void subscribe(String topic, MqttTopicHandler handler) throws MqttException { + handlers.put(topic, handler); + client.subscribe(topic); + } + + @Override + public void messageArrived(String topic, MqttMessage message) { + // Find the handler for this topic and delegate the work + if (handlers.containsKey(topic)) { + handlers.get(topic).handleMqtt(topic, message); + } + } + + @Override + public void connectionLost(Throwable cause) { + /* Handle Reconnect */ + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) {} +} diff --git a/apps/backend/src/main/java/org/yamcs/mrt/utils/MqttTopicHandler.java b/apps/backend/src/main/java/org/yamcs/mrt/utils/MqttTopicHandler.java new file mode 100644 index 0000000..513b39d --- /dev/null +++ b/apps/backend/src/main/java/org/yamcs/mrt/utils/MqttTopicHandler.java @@ -0,0 +1,7 @@ +package org.yamcs.mrt.utils; + +import org.eclipse.paho.client.mqttv3.MqttMessage; + +public interface MqttTopicHandler { + void handleMqtt(String topic, MqttMessage message); +} diff --git a/apps/backend/src/main/yamcs/etc/processor.yaml b/apps/backend/src/main/yamcs/etc/processor.yaml index 17c98d3..7b6bbea 100644 --- a/apps/backend/src/main/yamcs/etc/processor.yaml +++ b/apps/backend/src/main/yamcs/etc/processor.yaml @@ -1,8 +1,33 @@ +realtime-urrg: + services: + - class: org.yamcs.StreamTmPacketProvider + args: + streams: ["tm_433_realtime", "system_a_fc_realtime"] + - class: org.yamcs.StreamTcCommandReleaser + - class: org.yamcs.tctm.StreamParameterProvider + - class: org.yamcs.algorithms.AlgorithmManager + - class: org.yamcs.parameter.LocalParameterManager + config: + subscribeAll: true + allowContainerlessCommands: true + # Check alarms and also enable the alarm server (that keeps track of unacknowledged alarms) + alarm: + parameterCheck: true + parameterServer: enabled + tmProcessor: + # If container entries fit outside the binary packet, setting this to true causes the error + # to be ignored, otherwise an exception will be printed in Yamcs log output + ignoreOutOfContainerEntries: false + # Record all the parameters that have initial values at the start of the processor + recordInitialValues: true + # Record the local values + recordLocalValues: true + realtime: services: - class: org.yamcs.StreamTmPacketProvider args: - streams: ["tm_433_realtime", "tm_903_realtime"] + streams: ["tm_thermocouple_realtime", "tm_controlbox_realtime"] - class: org.yamcs.StreamTcCommandReleaser - class: org.yamcs.tctm.StreamParameterProvider - class: org.yamcs.algorithms.AlgorithmManager diff --git a/apps/backend/src/main/yamcs/etc/yamcs.ground_station.yaml b/apps/backend/src/main/yamcs/etc/yamcs.ground_station.yaml index 707c6b3..51e647c 100644 --- a/apps/backend/src/main/yamcs/etc/yamcs.ground_station.yaml +++ b/apps/backend/src/main/yamcs/etc/yamcs.ground_station.yaml @@ -22,91 +22,58 @@ services: - class: org.yamcs.timeline.TimelineService dataLinks: - - name: ASTRA-433 - class: org.yamcs.mrt.AstraAggregateDataLink - autoReconnect: true - brokers: - - tcp://localhost:1883 - frequency: "435.00" - tmStream: tm_433_realtime - - - name: ASTRA-903 - class: org.yamcs.mrt.AstraAggregateDataLink - autoReconnect: true - brokers: - - tcp://localhost:1883 - frequency: "903.00" - tmStream: tm_903_realtime - - - name: ASTRA-Other - class: org.yamcs.mrt.AstraAggregateDataLink - autoReconnect: true - brokers: - - tcp://localhost:1883 - frequency: "N/A" - tmStream: tm_daq_realtime - - - name: ASTRA-Commands - class: org.yamcs.mrt.AstraCommandLink - autoReconnect: true - brokers: - - tcp://localhost:1883 - tcStream: tc_realtime - - name: LabJack class: org.yamcs.labjack.LabJackDataLink tmStream: tm_labJack tcStream: tc_labJack + packetPreprocessorClassName: org.yamcs.mrt.utils.AstraPacketPreprocessor + packetPreprocessorArgs: + timestampOffset: 2 - - name: simulator - class: org.yamcs.tctm.UdpParameterDataLink - stream: pp_realtime - port: 11016 - json: true + + - name: Thermocouple + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: tm_thermocouple_realtime + packetPreprocessorClassName: org.yamcs.mrt.utils.AstraPacketPreprocessor + packetPreprocessorArgs: + timestampOffset: 2 + + - name: ControlBox + class: org.yamcs.mrt.links.ControlBoxLink + stream: tm_controlbox_realtime + packetPreprocessorClassName: org.yamcs.mrt.utils.AstraPacketPreprocessor + packetPreprocessorArgs: + timestampOffset: 2 mdb: - - type: emptyNode - spec: "FC903" - subLoaders: - - type: xtce - args: - file: mdb/rocket.xml - - type: emptyNode - spec: "FC435" - subLoaders: - - type: xtce - args: - file: mdb/rocket.xml - type: xtce args: - file: mdb/commands.xml + file: mdb/controlbox.xml - type: xtce spec: "LabJack" args: file: mdb/labjack-t7.xml - - type: xtce args: file: mdb/thermocouple.xml + # Configuration for streams created at server startup streamConfig: tm: - - name: "tm_433_realtime" - rootContainer: "/FC435/FlightComputer/FCFrame" - processor: "realtime" - - name: "tm_903_realtime" - rootContainer: "/FC903/FlightComputer/FCFrame" - processor: "realtime" - - name: "tm_daq_realtime" + - name: "tm_thermocouple_realtime" rootContainer: "/Thermocouple/ThermocoupleAggregatePacket" processor: "realtime" - - name: "tm_dump" + - name: "tm_controlbox_realtime" + rootContainer: "/ControlBox/ControlBoxPacket" + processor: "realtime" # Labjack - name: "tm_labJack" processor: "realtime" rootContainer: "/LabJackT7/LabJackPacket" + - name: "tm_dump" + cmdHist: ["cmdhist_realtime", "cmdhist_dump"] event: ["events_realtime", "events_dump"] param: ["pp_realtime", "sys_param", "proc_param"] @@ -115,7 +82,3 @@ streamConfig: - name: "tc_labJack" processor: "realtime" tcPatterns: ["/LabJackT7/.*"] - - - name: "tc_realtime" - processor: "realtime" - tcPatterns: ["/FlightComputer/.*"] diff --git a/apps/backend/src/main/yamcs/etc/yamcs.launch-canada.yaml b/apps/backend/src/main/yamcs/etc/yamcs.launch-canada.yaml new file mode 100644 index 0000000..bc760ae --- /dev/null +++ b/apps/backend/src/main/yamcs/etc/yamcs.launch-canada.yaml @@ -0,0 +1,120 @@ +services: + - class: org.yamcs.archive.XtceTmRecorder + - class: org.yamcs.archive.ParameterRecorder + - class: org.yamcs.archive.AlarmRecorder + - class: org.yamcs.archive.EventRecorder + - class: org.yamcs.archive.ReplayServer + - class: org.yamcs.parameter.SystemParametersService + args: + producers: ["jvm", "fs", "diskstats", "rocksdb"] + - class: org.yamcs.ProcessorCreatorService + args: + name: realtime + type: realtime + - class: org.yamcs.archive.CommandHistoryRecorder + - class: org.yamcs.parameterarchive.ParameterArchive + args: + realtimeFiller: + enabled: true + backFiller: + enabled: false + warmupTime: 60 + - class: org.yamcs.timeline.TimelineService + +dataLinks: + - name: simulator + class: org.yamcs.tctm.UdpParameterDataLink + stream: pp_realtime + port: 11016 + json: true + + - name: SystemA/Rocket/FlightComputer + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + - name: SystemA/ControlStation/Radio + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + - name: SystemA/Pad/Radio + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + + - name: SystemB/Rocket/FlightComputer + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + - name: SystemB/ControlStation/Radio + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + - name: SystemB/Pad/Radio + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + +mdb: + - type: xtce + args: + file: mdb/commands.xml + - type: emptyNode + spec: "SystemA" + subLoaders: + - type: emptyNode + spec: "Rocket" + subLoaders: + - type: xtce + args: + file: mdb/rocket.xml + + - type: emptyNode + spec: "ControlStation" + subLoaders: + - type: xtce + args: + file: mdb/radio.xml + - type: emptyNode + spec: "Pad" + subLoaders: + - type: xtce + args: + file: mdb/radio.xml + + - type: emptyNode + spec: "SystemB" + subLoaders: + - type: emptyNode + spec: "Rocket" + subLoaders: + - type: xtce + args: + file: mdb/rocket.xml + - type: emptyNode + spec: "ControlStation" + subLoaders: + - type: xtce + args: + file: mdb/radio.xml + - type: emptyNode + spec: "Pad" + subLoaders: + - type: xtce + args: + file: mdb/radio.xml + +# Configuration for streams created at server startup +streamConfig: + tm: + - name: "tm_433_realtime" + rootContainer: "/SystemA/Rocket/FlightComputer/FCFrame" + processor: "realtime" + - name: "tm_903_realtime" + rootContainer: "/SystemA/Rocket/FlightComputer/FCFrame" + processor: "realtime" + # - name: "tm_daq_realtime" + # rootContainer: "/Thermocouple/ThermocoupleAggregatePacket" + # processor: "realtime" + + cmdHist: ["cmdhist_realtime", "cmdhist_dump"] + event: ["events_realtime", "events_dump"] + param: ["pp_realtime", "sys_param", "proc_param"] + parameterAlarm: ["alarms_realtime"] + tc: + - name: "tc_realtime" + processor: "realtime" + tcPatterns: ["/FlightComputer/.*"] diff --git a/apps/backend/src/main/yamcs/etc/yamcs.urrg.yaml b/apps/backend/src/main/yamcs/etc/yamcs.urrg.yaml new file mode 100644 index 0000000..b6b960f --- /dev/null +++ b/apps/backend/src/main/yamcs/etc/yamcs.urrg.yaml @@ -0,0 +1,117 @@ +services: + - class: org.yamcs.archive.XtceTmRecorder + - class: org.yamcs.archive.ParameterRecorder + - class: org.yamcs.archive.AlarmRecorder + - class: org.yamcs.archive.EventRecorder + - class: org.yamcs.archive.ReplayServer + - class: org.yamcs.parameter.SystemParametersService + args: + producers: ["jvm", "fs", "diskstats", "rocksdb"] + - class: org.yamcs.ProcessorCreatorService + args: + name: realtime + type: realtime-urrg + - class: org.yamcs.archive.CommandHistoryRecorder + - class: org.yamcs.parameterarchive.ParameterArchive + args: + realtimeFiller: + enabled: true + backFiller: + enabled: false + warmupTime: 60 + - class: org.yamcs.timeline.TimelineService + +dataLinks: + - name: simulator + class: org.yamcs.tctm.UdpParameterDataLink + stream: pp_realtime + port: 11016 + json: true + + - name: SystemA/Rocket/FlightComputer + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: system_a_fc_realtime + packetPreprocessorClassName: org.yamcs.mrt.utils.AstraPacketPreprocessor + packetPreprocessorArgs: + timestampOffset: 2 + - name: SystemA/ControlStation/Radio + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: system_a_radio_realtime + packetPreprocessorClassName: org.yamcs.mrt.utils.AstraPacketPreprocessor + packetPreprocessorArgs: + timestampOffset: 2 + + - name: SystemB/Rocket/FlightComputer + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + packetPreprocessorClassName: org.yamcs.mrt.utils.AstraPacketPreprocessor + packetPreprocessorArgs: + timestampOffset: 2 + - name: SystemB/ControlStation/Radio + class: org.yamcs.mrt.links.AstraGenericTmLink + stream: pp_realtime + packetPreprocessorClassName: org.yamcs.mrt.utils.AstraPacketPreprocessor + packetPreprocessorArgs: + timestampOffset: 2 + +mdb: + - type: xtce + args: + file: mdb/commands.xml + - type: emptyNode + spec: "SystemA" + subLoaders: + - type: emptyNode + spec: "Rocket" + subLoaders: + - type: xtce + args: + file: mdb/rocket.xml + + - type: emptyNode + spec: "ControlStation" + subLoaders: + - type: xtce + args: + file: mdb/radio.xml + + - type: emptyNode + spec: "SystemB" + subLoaders: + - type: emptyNode + spec: "Rocket" + subLoaders: + - type: xtce + args: + file: mdb/rocket.xml + - type: emptyNode + spec: "ControlStation" + subLoaders: + - type: xtce + args: + file: mdb/radio.xml + +# Configuration for streams created at server startup +streamConfig: + tm: + - name: "tm_433_realtime" + rootContainer: "/SystemA/Rocket/FlightComputer/FCFrame" + processor: "realtime" + - name: "system_a_fc_realtime" + rootContainer: "/SystemA/Rocket/FlightComputer/FCFrame" + processor: "realtime" + - name: "system_a_radio_realtime" + rootContainer: "/SystemA/ControlStation/Radio/TelemetryPacket" + processor: "realtime" + # - name: "tm_daq_realtime" + # rootContainer: "/Thermocouple/ThermocoupleAggregatePacket" + # processor: "realtime" + + cmdHist: ["cmdhist_realtime", "cmdhist_dump"] + event: ["events_realtime", "events_dump"] + param: ["pp_realtime", "sys_param", "proc_param"] + parameterAlarm: ["alarms_realtime"] + tc: + - name: "tc_realtime" + processor: "realtime" + tcPatterns: ["/FlightComputer/.*"] diff --git a/apps/backend/src/main/yamcs/etc/yamcs.yaml b/apps/backend/src/main/yamcs/etc/yamcs.yaml index 8cf0b3e..3ba786b 100644 --- a/apps/backend/src/main/yamcs/etc/yamcs.yaml +++ b/apps/backend/src/main/yamcs/etc/yamcs.yaml @@ -8,7 +8,9 @@ services: dataDir: yamcs-data instances: + # - urrg - ground_station + # - testsite # Secret key unique to a particular Yamcs installation. # This is used to provide cryptographic signing. diff --git a/apps/backend/src/main/yamcs/mdb/commands.xml b/apps/backend/src/main/yamcs/mdb/commands.xml index 0012d57..493bf52 100644 --- a/apps/backend/src/main/yamcs/mdb/commands.xml +++ b/apps/backend/src/main/yamcs/mdb/commands.xml @@ -6,171 +6,221 @@ See https://github.com/yamcs/pymdb - - - - - - - - + ARM Recovery - + + + + + Umbilical High + + + + + DISARM Recovery - + - + + Drogue Ejection - + - + + Emergency Cancel - + - + + Emergency Stop - + - + + FDOV De-energize - + - + + FDOV Energize - + + Landed - + + Launch - + - + + Main Ejection - + + MOV Arming - + + MOV Disarming - + + Propulsion Off - + + Propulsion On - + - - - - - - - - - 0 - - - + + Radio Normal TX Speed + + - + - - + + Radio Slow TX Speed + - + - + + Resets the FC + + + + + + + + Reset Prop Valve States - + + + + + Close SD Card + + + + + + + + Deletes SD Data + + + + + + + + Arm SD Deletion + + + + - - - - - - - - - 0 - - - + + Disarm SD Deletion + + + + + + + + Open SD Card + + + + + + + + Umbilical Low + + - + - + + Vent Valve De-energize - + - + + Vent Valve Energize - + diff --git a/apps/backend/src/main/yamcs/mdb/controlbox.xml b/apps/backend/src/main/yamcs/mdb/controlbox.xml new file mode 100644 index 0000000..afd8c94 --- /dev/null +++ b/apps/backend/src/main/yamcs/mdb/controlbox.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + + 0 + + + + + + 0 + + + + + 0 + + + + + + 0 + + + + + 0 + + + + + + 0 + + + + + 0 + + + + + + 0 + + + + + 0 + + + + + + 0 + + + + + 0 + + + + + + 0 + + + + + + + diff --git a/apps/backend/src/main/yamcs/mdb/radio.xml b/apps/backend/src/main/yamcs/mdb/radio.xml new file mode 100644 index 0000000..8100f49 --- /dev/null +++ b/apps/backend/src/main/yamcs/mdb/radio.xml @@ -0,0 +1,76 @@ + + + + + + + + + dBm + + + + + + -2 + / + + + + + + + dB + + + + + + 4 + / + + + + + + + + A.S.T.R.A. Packet Identifider + + + + + Relative Signal Strength Indicator of the ground radio + + + + + + + + + + 0 + + + + + 0 + + + + + 0 + + + + + + + diff --git a/apps/backend/src/main/yamcs/mdb/rocket.xml b/apps/backend/src/main/yamcs/mdb/rocket.xml index 450dcdc..1925022 100644 --- a/apps/backend/src/main/yamcs/mdb/rocket.xml +++ b/apps/backend/src/main/yamcs/mdb/rocket.xml @@ -6,65 +6,85 @@ See https://github.com/yamcs/pymdb - + + + g + - 100 + 1000 / - - + + + + g + - 100 + 1000 / - - + + + + g + - 100 + 1000 / - - - - - - - - - - - - - - - - + + ft + + - - + + + ft + + - - + + + ft + + - - + + + mA + + + + + + V + + + + + + 10 + / + + + @@ -76,26 +96,62 @@ See https://github.com/yamcs/pymdb - + psi - - - + + + + + + + + + + + + + + + + + + + + + + bar + + - 2 - + - 400 + 1000 + / + + + + + + + dBm + + + + + + -2 / - - + + + + dB + @@ -105,7 +161,21 @@ See https://github.com/yamcs/pymdb - + + + + °C + + + + + + 100 + / + + + + @@ -149,59 +219,137 @@ See https://github.com/yamcs/pymdb - + + m + + + + + + 1000 + / + + + - + + deg + + + + + + 10000000 + / + + + - + + deg + + + + + + 10000000 + / + + + - + + s + + - + + + deg/s + + + + + + 1000 + / + + + + + + + deg/s + + + + + + 1000 + / + + + + + + + deg/s + + + + + + 1000 + / + + + + + - + - + - + - + - + - + - - - - + - + - + - + - + - + + + + @@ -211,42 +359,24 @@ See https://github.com/yamcs/pymdb - - - - - - - - - - - - - - - - - - - + psi - - + + °C - + @@ -265,7 +395,7 @@ See https://github.com/yamcs/pymdb - + °C @@ -278,79 +408,98 @@ See https://github.com/yamcs/pymdb - + - + + ft/s + + - + - + - + - - Calculated on FC + + Calculated on the FC in order to determine ejection. - - Calculated on FC + + Initalized by the flight computer when it started or restarted. - + - + - + + Current voltage of the FC battery - + - + + Calculated from sensor voltage. - + - + - + - - Calculated from sensor voltage. + + + + - + - + + Barometric pressure measured by the FC - + + Relative Signal Strength Indicator of FC radio - + - + + Temperature measured by the FC - + - + - + + + + + + + + + + @@ -372,87 +521,78 @@ See https://github.com/yamcs/pymdb The current stage the FC believes it is in. - - - - + - + - + - + - + - + - + - + - + - - Calculated from sensor voltage. + - - A.S.T.R.A. Packet Padding - - - + - + - + - - - - + - + - + - + - + - + - + + Calculated from hall state sensor voltage. - + + A.S.T.R.A. Packet Padding - + - + - + @@ -490,7 +630,7 @@ See https://github.com/yamcs/pymdb - + @@ -567,32 +707,27 @@ See https://github.com/yamcs/pymdb 0 - - - 0 - - - + 0 - + 0 - + 0 - + 0 - + 0 @@ -637,17 +772,17 @@ See https://github.com/yamcs/pymdb 0 - + 0 - + 0 - + 0 @@ -662,6 +797,16 @@ See https://github.com/yamcs/pymdb 0 + + + 0 + + + + + 0 + + @@ -813,62 +958,62 @@ See https://github.com/yamcs/pymdb 11 - + 10 - + 9 - + 8 - + 23 - + 22 - + 21 - + 20 - + 19 - + 18 - + 17 - + 16 - + 31 @@ -912,228 +1057,5 @@ See https://github.com/yamcs/pymdb - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - == - True - - - - - - - - == - True - - - - - - - - - - - - - - - - - == - True - - - - - - - - == - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/apps/frontend/src/cards/command-button/index.tsx b/apps/frontend/src/cards/command-button/index.tsx index 7d8a709..5deaa14 100644 --- a/apps/frontend/src/cards/command-button/index.tsx +++ b/apps/frontend/src/cards/command-button/index.tsx @@ -5,7 +5,12 @@ import { DataGridRow, } from "@/components/ui/data-grid"; import { makeCard } from "@/lib/cards"; -import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react"; +import { + Result, + useAtomSet, + useAtomSuspense, + useAtomValue, +} from "@effect-atom/atom-react"; import { YamcsAtomClient } from "@mrt/yamcs-atom"; import { Cause, Schema } from "effect"; @@ -13,29 +18,6 @@ type Command = { name: string; }; -const commands: Command[] = [ - { name: "FlightComputer/arm_recovery" }, - { name: "FlightComputer/disarm_recovery" }, - { name: "FlightComputer/drogue_ejection" }, - { name: "FlightComputer/emergency_cancel" }, - { name: "FlightComputer/emergency_stop" }, - { name: "FlightComputer/fdov_de-energize" }, - { name: "FlightComputer/fdov_energize" }, - { name: "FlightComputer/landed" }, - { name: "FlightComputer/launch" }, - { name: "FlightComputer/main_ejection" }, - { name: "FlightComputer/mov_arming" }, - { name: "FlightComputer/mov_disarming" }, - { name: "FlightComputer/propulsion_off" }, - { name: "FlightComputer/propulsion_on" }, - { name: "FlightComputer/radio_set_transmit" }, - { name: "FlightComputer/reset_from_start" }, - { name: "FlightComputer/reset_prop_boards_valve_state" }, - { name: "FlightComputer/umbilical" }, - { name: "FlightComputer/vent_valve_de-energize" }, - { name: "FlightComputer/vent_valve_energize" }, -]; - export const CommandButtonCard = makeCard({ id: "command-button", name: "Command Button Card", @@ -68,6 +50,13 @@ function CommandButtonTable() { YamcsAtomClient.mutation("command", "issueCommand"), ); + const { commands } = useAtomSuspense( + YamcsAtomClient.query("mdb", "listCommands", { + path: { instance: "ground_station" }, + urlParams: {}, + }), + ).value; + return (

@@ -79,14 +68,17 @@ function CommandButtonTable() { {commands.map((command) => ( -
{command.name.split("/")[1]}
+
+ {command.longDescription ?? command.qualifiedName}{" "} + {command.shortDescription && `(${command.shortDescription})`} +