001package jmri.jmrix.mqtt;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import javax.annotation.Nonnull;
006
007import jmri.Consist;
008import jmri.ConsistListener;
009import jmri.DccLocoAddress;
010
011/**
012 * This is the Consist definition for a consist on an MQTT system.
013 *
014 * @author Dean Cording Copyright (C) 2023
015 */
016public class MqttConsist extends jmri.implementation.DccConsist {
017
018    private final MqttAdapter mqttAdapter;
019    @Nonnull
020    public String sendTopicPrefix = "cab/{0}/consist";
021    private boolean active = false;
022
023    // Initialize a consist for the specific address.
024    // The Default consist type is controller consist
025    public MqttConsist(int address, MqttSystemConnectionMemo memo, String sendTopicPrefix) {
026        super(address);
027        mqttAdapter = memo.getMqttAdapter();
028        this.sendTopicPrefix = sendTopicPrefix;
029        consistType = Consist.CS_CONSIST;
030        log.debug("Consist {} created.", this.getConsistAddress());
031    }
032
033    // Initialize a consist for the specific address.
034    // The Default consist type is controller consist
035    public MqttConsist(DccLocoAddress address, MqttSystemConnectionMemo memo, String sendTopicPrefix) {
036        super(address);
037        mqttAdapter = memo.getMqttAdapter();
038        this.sendTopicPrefix = sendTopicPrefix;
039        consistType = Consist.CS_CONSIST;
040        log.debug("Consist {} created.", this.getConsistAddress());
041    }
042
043    // Clean Up local storage.
044    @Override
045    public void dispose() {
046        super.dispose();
047        log.debug("Consist {} disposed.", this.getConsistAddress());
048    }
049
050    // Set the Consist Type.
051    @Override
052    public void setConsistType(int consist_type) {
053        log.debug("Set Consist Type {}", consist_type);
054        if (consist_type == Consist.CS_CONSIST) {
055            consistType = consist_type;
056        } else {
057            log.error("Consist Type Not Supported");
058            notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented);
059        }
060    }
061
062    /**
063     * Is this address allowed?
064     * On MQTT systems, all addresses but 0 can be used in a consist.
065     * {@inheritDoc}
066     */
067    @Override
068    public boolean isAddressAllowed(DccLocoAddress address) {
069        return address.getNumber() != 0;
070    }
071
072    /**
073     * Is there a size limit for this consist?
074     *
075     * @return -1 for Controller Consists (no limit),
076     * 0 for any other consist type
077     */
078    @Override
079    public int sizeLimit() {
080        if (consistType == CS_CONSIST) {
081            return -1;
082        } else {
083            return 0;
084        }
085    }
086
087    /**
088     * Does the consist contain the specified address?
089     * {@inheritDoc}
090     */
091    @Override
092    public boolean contains(DccLocoAddress address) {
093        if (consistType == CS_CONSIST) {
094            return consistList.contains(address);
095        } else {
096            log.error("Consist Type Not Supported");
097            notifyConsistListeners(address, ConsistListener.NotImplemented);
098        }
099        return false;
100    }
101
102    /**
103     * Get the relative direction setting for a specific
104     * locomotive in the consist.
105     * {@inheritDoc}
106     */
107    @Override
108    public boolean getLocoDirection(DccLocoAddress address) {
109        if (consistType == CS_CONSIST) {
110            return consistDir.getOrDefault(address, false);
111        } else {
112            log.error("Consist Type Not Supported");
113            notifyConsistListeners(address, ConsistListener.NotImplemented);
114        }
115        return false;
116    }
117
118    /**
119     * Add an Address to the internal consist list object.
120     */
121    private synchronized void addToConsistList(DccLocoAddress locoAddress, boolean directionNormal) {
122
123        log.debug("Add to consist list address {} direction {}", locoAddress, directionNormal);
124        if (!(consistList.contains(locoAddress))) {
125            consistList.add(locoAddress);
126        }
127        consistDir.put(locoAddress, directionNormal);
128        notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
129    }
130
131    /**
132     * Remove an address from the internal consist list object.
133     */
134    private synchronized void removeFromConsistList(DccLocoAddress locoAddress) {
135        log.debug("Remove from consist list address {}", locoAddress);
136        consistDir.remove(locoAddress);
137        consistList.remove(locoAddress);
138        notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
139    }
140
141    /**
142     * Add a Locomotive to a Consist.
143     *
144     * @param locoAddress is the Locomotive address to add to the locomotive
145     * @param directionNormal is True if the locomotive is traveling
146     *        the same direction as the consist, or false otherwise.
147     */
148    @Override
149    public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) {
150        log.debug("Add to consist address {} direction {}", locoAddress, directionNormal);
151        if (consistType == CS_CONSIST) {
152            addToConsistList(locoAddress, directionNormal);
153            if (active) {
154                publish();
155            }
156        } else {
157            log.error("Consist Type Not Supported");
158            notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
159        }
160    }
161
162    /**
163     * Restore a Locomotive to Consist, but don't write to
164     * the command station.  This is used for restoring the consist
165     * from a file or adding a consist read from the command station.
166     *
167     * @param locoAddress is the Locomotive address to add to the locomotive
168     * @param directionNormal is True if the locomotive is traveling
169     *        the same direction as the consist, or false otherwise.
170     */
171    @Override
172    public synchronized void restore(DccLocoAddress locoAddress, boolean directionNormal) {
173        log.debug("Restore to consist address {} direction {}", locoAddress, directionNormal);
174
175        if (consistType == CS_CONSIST) {
176            addToConsistList(locoAddress, directionNormal);
177        } else {
178            log.error("Consist Type Not Supported");
179            notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
180        }
181    }
182
183    /**
184     * Remove a Locomotive from this Consist.
185     *
186     * @param locoAddress is the Locomotive address to add to the locomotive
187     */
188    @Override
189    public synchronized void remove(DccLocoAddress locoAddress) {
190        log.debug("Remove from consist address {}", locoAddress);
191
192        if (consistType == CS_CONSIST) {
193            removeFromConsistList(locoAddress);
194            if (active) {
195                publish();
196            }
197        } else {
198            log.error("Consist Type Not Supported");
199            notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
200        }
201    }
202
203    /**
204     * Activates the consist for use with a throttle
205     */
206    public void activate(){
207
208        log.info("Activating consist {}", consistID);
209        active = true;
210        publish();
211    }
212
213    /**
214     * Deactivates and removes the consist from a throttle
215     */
216     public void deactivate() {
217
218        log.info("Deactivating consist {}", consistID);
219        active = false;
220        // Clear MQTT message
221        jmri.util.ThreadingUtil.runOnLayoutEventually(() ->
222            mqttAdapter.publish(this.sendTopicPrefix.replaceFirst("\\{0\\}", 
223                String.valueOf(consistAddress.getNumber())), ""));
224
225    }
226
227    @SuppressFBWarnings(value = "WMI_WRONG_MAP_ITERATOR", justification = "false positive")
228    private String getConsistMakeup() {
229
230        String consistMakeup = "";
231
232        for (DccLocoAddress  address : consistDir.keySet()) {
233            consistMakeup = consistMakeup.concat(consistDir.get(address) ? "":"-")
234                .concat(String.valueOf(address.getNumber())).concat(" ");
235        }
236
237        return consistMakeup.trim();
238
239    }
240
241    /**
242     * Publish the consist details to the controller
243     */
244    private void publish(){
245        // Send MQTT message
246        jmri.util.ThreadingUtil.runOnLayout(() ->
247            mqttAdapter.publish(this.sendTopicPrefix.replaceFirst("\\{0\\}", 
248                String.valueOf(consistAddress.getNumber())), getConsistMakeup()));
249
250    }
251
252    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MqttConsist.class);
253
254}