001package jmri.jmrix.mqtt;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import jmri.Consist;
005import jmri.ConsistListener;
006import jmri.DccLocoAddress;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009import javax.annotation.Nonnull;
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     */
066    @Override
067    public boolean isAddressAllowed(DccLocoAddress address) {
068        if (address.getNumber() != 0) {
069            return (true);
070        } else {
071            return (false);
072        }
073    }
074
075    /**
076     * Is there a size limit for this consist?
077     *
078     * @return -1 for Controller Consists (no limit),
079     * 0 for any other consist type
080     */
081    @Override
082    public int sizeLimit() {
083        if (consistType == CS_CONSIST) {
084            return -1;
085        } else {
086            return 0;
087        }
088    }
089
090    /**
091     * Does the consist contain the specified address?
092     */
093    @Override
094    public boolean contains(DccLocoAddress address) {
095        if (consistType == CS_CONSIST) {
096            return consistList.contains(address);
097        } else {
098            log.error("Consist Type Not Supported");
099            notifyConsistListeners(address, ConsistListener.NotImplemented);
100        }
101        return false;
102    }
103
104    /**
105     * Get the relative direction setting for a specific
106     * locomotive in the consist.
107     */
108    @Override
109    public boolean getLocoDirection(DccLocoAddress address) {
110        if (consistType == CS_CONSIST) {
111            Boolean Direction = consistDir.get(address);
112            return (Direction.booleanValue());
113        } else {
114            log.error("Consist Type Not Supported");
115            notifyConsistListeners(address, ConsistListener.NotImplemented);
116        }
117        return false;
118    }
119
120    /**
121     * Add an Address to the internal consist list object.
122     */
123    private synchronized void addToConsistList(DccLocoAddress LocoAddress, boolean directionNormal) {
124
125        log.debug("Add to consist list address {} direction {}", LocoAddress, directionNormal);
126        Boolean Direction = Boolean.valueOf(directionNormal);
127        if (!(consistList.contains(LocoAddress))) {
128            consistList.add(LocoAddress);
129        }
130        consistDir.put(LocoAddress, Direction);
131        notifyConsistListeners(LocoAddress, ConsistListener.OPERATION_SUCCESS);
132    }
133
134    /**
135     * Remove an address from the internal consist list object.
136     */
137    private synchronized void removeFromConsistList(DccLocoAddress LocoAddress) {
138        log.debug("Remove from consist list address {}", LocoAddress);
139        consistDir.remove(LocoAddress);
140        consistList.remove(LocoAddress);
141        notifyConsistListeners(LocoAddress, ConsistListener.OPERATION_SUCCESS);
142    }
143
144    /**
145     * Add a Locomotive to a Consist.
146     *
147     * @param LocoAddress is the Locomotive address to add to the locomotive
148     * @param directionNormal is True if the locomotive is traveling
149     *        the same direction as the consist, or false otherwise.
150     */
151    @Override
152    public synchronized void add(DccLocoAddress LocoAddress, boolean directionNormal) {
153        log.debug("Add to consist address {} direction {}", LocoAddress, directionNormal);
154        if (consistType == CS_CONSIST) {
155            addToConsistList(LocoAddress, directionNormal);
156            if (active) publish();
157        } else {
158            log.error("Consist Type Not Supported");
159            notifyConsistListeners(LocoAddress, ConsistListener.NotImplemented);
160        }
161    }
162
163    /**
164     * Restore a Locomotive to Consist, but don't write to
165     * the command station.  This is used for restoring the consist
166     * from a file or adding a consist read from the command station.
167     *
168     * @param LocoAddress is the Locomotive address to add to the locomotive
169     * @param directionNormal is True if the locomotive is traveling
170     *        the same direction as the consist, or false otherwise.
171     */
172    @Override
173    public synchronized void restore(DccLocoAddress LocoAddress, boolean directionNormal) {
174        log.debug("Restore to consist address {} direction {}", LocoAddress, directionNormal);
175
176        if (consistType == CS_CONSIST) {
177            addToConsistList(LocoAddress, directionNormal);
178        } else {
179            log.error("Consist Type Not Supported");
180            notifyConsistListeners(LocoAddress, ConsistListener.NotImplemented);
181        }
182    }
183
184    /**
185     * Remove a Locomotive from this Consist.
186     *
187     * @param LocoAddress is the Locomotive address to add to the locomotive
188     */
189    @Override
190    public synchronized void remove(DccLocoAddress LocoAddress) {
191        log.debug("Remove from consist address {}", LocoAddress);
192
193        if (consistType == CS_CONSIST) {
194            removeFromConsistList(LocoAddress);
195            if (active) publish();
196        } else {
197            log.error("Consist Type Not Supported");
198            notifyConsistListeners(LocoAddress, ConsistListener.NotImplemented);
199        }
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\\}", String.valueOf(consistAddress.getNumber())), "");
223        });
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) ? "":"-").concat(String.valueOf(address.getNumber())).concat(" ");
234        }
235
236        return consistMakeup.trim();
237
238    }
239
240    /**
241     * Publish the consist details to the controller
242     */
243    private void publish(){
244           // Send MQTT message
245            jmri.util.ThreadingUtil.runOnLayout(() -> {
246                mqttAdapter.publish(this.sendTopicPrefix.replaceFirst("\\{0\\}", String.valueOf(consistAddress.getNumber())), getConsistMakeup());
247            });
248
249    }
250
251    private final static Logger log = LoggerFactory.getLogger(MqttConsist.class);
252
253}