001package jmri.jmrix.tmcc;
002
003import javax.annotation.Nonnull;
004
005import jmri.Consist;
006import jmri.ConsistListener;
007import jmri.LocoAddress;
008import jmri.DccLocoAddress;
009
010/**
011 * This is the Consist definition for a consist on a TMCC system.
012 *
013 * Based on MqttConsist by
014 * @author Dean Cording Copyright (C) 2023
015 * with edits/additions by
016 * @author Timothy Jump (C) 2025
017 */
018
019public class TmccConsist extends jmri.implementation.DccConsist {
020
021    @Nonnull
022    public String sendTopicPrefix = "cab/{0}/consist";
023    private boolean active = false;
024
025    // Initialize a consist for the specific address.
026    // The Default consist type is controller consist
027    public TmccConsist(int address, TmccSystemConnectionMemo memo, String sendTopicPrefix) {
028        super(address);
029          tc = memo.getTrafficController();
030        this.sendTopicPrefix = sendTopicPrefix;
031        consistType = Consist.CS_CONSIST;
032        log.debug("Consist {} created.", this.getConsistAddress());
033    }
034
035    // Initialize a consist for the specific address.
036    // The Default consist type is controller consist
037    public TmccConsist(DccLocoAddress address, TmccSystemConnectionMemo memo, String sendTopicPrefix) {
038        super(address);
039          tc = memo.getTrafficController();
040        this.sendTopicPrefix = sendTopicPrefix;
041        consistType = Consist.CS_CONSIST;
042        log.debug("Consist {} created.", this.getConsistAddress());
043    }
044
045    // Clean Up local storage.
046    @Override
047    public void dispose() {
048        super.dispose();
049        log.debug("Consist {} disposed.", this.getConsistAddress());
050    }
051
052    // Set the Consist Type.
053    @Override
054    public void setConsistType(int consist_type) {
055        log.debug("Set Consist Type {}", consist_type);
056        if (consist_type == Consist.CS_CONSIST) {
057            consistType = consist_type;
058        } else {
059            log.error("Consist Type Not Supported");
060            notifyConsistListeners(new DccLocoAddress(0, false), ConsistListener.NotImplemented);
061        }
062    }
063
064    protected SerialTrafficController tc = null;
065
066
067    /**
068     * Is this TMCC ENG (loco ID#) address allowed?
069     * TMCC systems only use ENG (loco ID#) addresses 1-98.
070     * {@inheritDoc}
071     */
072    @Override
073    public boolean isAddressAllowed(DccLocoAddress address) {
074        return address.getNumber() > 0 && address.getNumber() < 99;
075    }
076
077    /**
078     * Is there a size limit for this consist?
079     *
080     * @return -1 for Controller Consists (no limit),
081     * 0 for any other consist type
082     */
083    @Override
084    public int sizeLimit() {
085        if (consistType == CS_CONSIST) {
086            return -1;
087        } else {
088            return 0;
089        }
090    }
091
092    /**
093     * Does the consist contain the specified address?
094     * {@inheritDoc}
095     */
096    @Override
097    public boolean contains(DccLocoAddress address) {
098        if (consistType == CS_CONSIST) {
099            return consistList.contains(address);
100        } else {
101            log.error("Consist Type Not Supported");
102            notifyConsistListeners(address, ConsistListener.NotImplemented);
103        }
104        return false;
105    }
106
107    /**
108     * Get the relative direction setting for a specific
109     * locomotive in the consist.
110     * {@inheritDoc}
111     */
112    @Override
113    public boolean getLocoDirection(DccLocoAddress address) {
114        if (consistType == CS_CONSIST) {
115            return consistDir.getOrDefault(address, false);
116        } else {
117            log.error("Consist Type Not Supported");
118            notifyConsistListeners(address, ConsistListener.NotImplemented);
119        }
120        return false;
121    }
122
123    /**
124     * Add an Address to the internal consist list object.
125     */
126    private synchronized void addToConsistList(DccLocoAddress locoAddress, boolean directionNormal) {
127
128        log.debug("Add to consist list address {} direction {}", locoAddress, directionNormal);
129        if (!(consistList.contains(locoAddress))) {
130            consistList.add(locoAddress);
131        }
132        consistDir.put(locoAddress, directionNormal);
133        notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
134    }
135
136    /**
137     * Remove an address from the internal consist list object.
138     */
139    private synchronized void removeFromConsistList(DccLocoAddress locoAddress) {
140        log.debug("Remove from consist list address {}", locoAddress);
141        consistDir.remove(locoAddress);
142        consistList.remove(locoAddress);
143        notifyConsistListeners(locoAddress, ConsistListener.OPERATION_SUCCESS);
144    }
145
146    /**
147     * Add a Locomotive to a Consist
148     *
149     * @param locoAddress - is the Locomotive address to add to the consist.
150     * @param directionNormal - is True if the locomotive is traveling the same direction as the consist, or false otherwise.
151     */
152    @Override
153    public synchronized void add(DccLocoAddress locoAddress, boolean directionNormal) {
154
155        // TMCC1 Consist Build
156        if (locoAddress.getProtocol() == LocoAddress.Protocol.TMCC1) {
157            SerialMessage c = new SerialMessage();
158            SerialMessage m = new SerialMessage();
159            SerialMessage n = new SerialMessage();
160            c.setOpCode(0xFE);
161            m.setOpCode(0xFE);
162            n.setOpCode(0xFE);
163
164            // TMCC has 6 commands for adding a loco to a consist: head, rear, and mid, plus direction
165            if (!contains(locoAddress)) {
166                // First loco to consist
167                if (consistList.isEmpty()) {
168                    // add head loco
169                    if (!directionNormal) {
170                        // TMCC1 - Assign as Head Unit/Reverse Direction
171                        c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
172                        m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
173                        n.putAsWord(0x0025 + locoAddress.getNumber() * 128);
174                    } else {
175                        // TMCC1 - Assign as Head Unit/Forward Direction
176                        c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
177                        m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
178                        n.putAsWord(0x0021 + locoAddress.getNumber() * 128);
179                    }
180                // send to command station (send twice is set, but number of sends may need to be adjusted depending on efficiency)
181                tc.sendSerialMessage(c, null);
182                tc.sendSerialMessage(m, null);
183                tc.sendSerialMessage(n, null);
184
185                // Second loco to consist
186                } else if (consistList.size() == 1) {
187                    // add rear loco
188                    if (!directionNormal) {
189                        // TMCC1 - Assign as Rear Unit/Reverse Direction
190                        c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
191                        m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
192                        n.putAsWord(0x0027 + locoAddress.getNumber() * 128);
193                    } else {
194                        // TMCC1 - Assign as Rear Unit/Forward Direction
195                        c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
196                        m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
197                        n.putAsWord(0x0023 + locoAddress.getNumber() * 128);
198                    }
199                // send to command station (send twice is set, but number of sends may need to be adjusted depending on efficiency)
200                tc.sendSerialMessage(c, null);
201                tc.sendSerialMessage(m, null);
202                tc.sendSerialMessage(n, null);
203
204                // Additional loco(s) to consist
205                } else {
206                    // add mid loco
207                    if (!directionNormal) {
208                        // TMCC1 - Assign as Mid Unit/Reverse Direction
209                        c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
210                        m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
211                        n.putAsWord(0x0026 + locoAddress.getNumber() * 128);
212                    } else {
213                        // TMCC1 - Assign as Mid Unit/Forward Direction
214                        c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
215                        m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
216                        n.putAsWord(0x0022 + locoAddress.getNumber() * 128);
217                    }
218                // send to command station (send twice is set, but number of sends may need to be adjusted depending on efficiency)
219                tc.sendSerialMessage(c, null);
220                tc.sendSerialMessage(m, null);
221                tc.sendSerialMessage(n, null);
222
223                }
224
225                // Add Loco to Consist List
226                addToConsistList(locoAddress, directionNormal);
227
228            } else {
229                log.error("Loco {} is already part of this consist {}", locoAddress, getConsistAddress());
230            }
231        }
232
233        // TMCC2 Consist Build
234        if (locoAddress.getProtocol() == LocoAddress.Protocol.TMCC2) {
235            SerialMessage c = new SerialMessage();
236            SerialMessage m = new SerialMessage();
237            SerialMessage n = new SerialMessage();
238            c.setOpCode(0xF8);
239            m.setOpCode(0xF8);
240            n.setOpCode(0xF8);
241
242            // TMCC has 6 commands for adding a loco to a consist: head, rear, and mid, plus direction
243            if (!contains(locoAddress)) {
244                // First loco to consist
245                if (consistList.isEmpty()) {
246                    // add head loco
247                    if (!directionNormal) {
248                        // TMCC1 - Assign as Head Unit/Reverse Direction
249                        c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
250                        m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
251                        n.putAsWord(0x0123 + locoAddress.getNumber() * 512);
252                    } else {
253                        // TMCC1 - Assign as Head Unit/Forward Direction
254                        c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
255                        m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
256                        n.putAsWord(0x0122 + locoAddress.getNumber() * 512);
257                    }
258                // send to command station (send twice is set, but number of sends may need to be adjusted depending on efficiency)
259                tc.sendSerialMessage(c, null);
260                tc.sendSerialMessage(m, null);
261                tc.sendSerialMessage(n, null);
262
263                // Second loco to consist
264                } else if (consistList.size() == 1) {
265                    // add rear loco
266                    if (!directionNormal) {
267                        // TMCC1 - Assign as Rear Unit/Reverse Direction
268                        c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
269                        m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
270                        n.putAsWord(0x0127 + locoAddress.getNumber() * 512);
271                    } else {
272                        // TMCC1 - Assign as Rear Unit/Forward Direction
273                        c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
274                        m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
275                        n.putAsWord(0x0126 + locoAddress.getNumber() * 512);
276                    }
277                // send to command station (send twice is set, but number of sends may need to be adjusted depending on efficiency)
278                tc.sendSerialMessage(c, null);
279                tc.sendSerialMessage(m, null);
280                tc.sendSerialMessage(n, null);
281
282                // Additional loco(s) to consist
283                } else {
284                    // add mid loco
285                    if (!directionNormal) {
286                        // TMCC1 - Assign as Mid Unit/Reverse Direction
287                        c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
288                        m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
289                        n.putAsWord(0x0125 + locoAddress.getNumber() * 512);
290                    } else {
291                        // TMCC1 - Assign as Mid Unit/Forward Direction
292                        c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
293                        m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
294                        n.putAsWord(0x0124 + locoAddress.getNumber() * 512);
295                    }
296                // send to command station (send twice is set, but number of sends may need to be adjusted depending on efficiency)
297                tc.sendSerialMessage(c, null);
298                tc.sendSerialMessage(m, null);
299                tc.sendSerialMessage(n, null);
300
301                }
302
303                // Add Loco to Consist List
304                addToConsistList(locoAddress, directionNormal);
305
306            } else {
307                log.error("Loco {} is already part of this consist {}", locoAddress, getConsistAddress());
308            }
309        }
310    }
311
312    /**
313     * Restore a Locomotive to Consist, but don't write to
314     * the command station.  This is used for restoring the consist
315     * from a file or adding a consist read from the command station.
316     *
317     * @param locoAddress is the Locomotive address to add to the locomotive
318     * @param directionNormal is True if the locomotive is traveling
319     *        the same direction as the consist, or false otherwise.
320     */
321    @Override
322    public synchronized void restore(DccLocoAddress locoAddress, boolean directionNormal) {
323        log.debug("Restore to consist address {} direction {}", locoAddress, directionNormal);
324
325        if (consistType == CS_CONSIST) {
326            addToConsistList(locoAddress, directionNormal);
327        } else {
328            log.error("Consist Type Not Supported");
329            notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
330        }
331    }
332
333    /**
334     * Remove a Locomotive from this Consist
335     * Clear Consist ID from Locomotive
336     * @param locoAddress is the Locomotive address to add to the locomotive
337     */
338    @Override
339    public synchronized void remove(DccLocoAddress locoAddress) {
340        log.debug("Remove from consist address {}", locoAddress);
341
342        // TMCC1 - Clear Consist ID from Locomotive
343        if (locoAddress.getProtocol() == LocoAddress.Protocol.TMCC1) {
344            SerialMessage c = new SerialMessage();
345            c.setOpCode(0xFE);
346            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128));
347            tc.sendSerialMessage(c, null);
348        }
349
350        // TMCC2 - Clear Consist ID from Locomotive
351        if (locoAddress.getProtocol() == LocoAddress.Protocol.TMCC2) {
352            SerialMessage c = new SerialMessage();
353            c.setOpCode(0xF8);
354            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512));
355            tc.sendSerialMessage(c, null);
356        }
357
358        // Remove Locomotive from this Consist
359        if (consistType == CS_CONSIST) {
360            removeFromConsistList(locoAddress);
361            if (active) {
362                publish();
363            }
364        } else {
365                log.error("Consist Type Not Supported");
366                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
367        }
368    }
369
370    /**
371     * Activates the consist for use with a throttle
372     */
373    public void activate(){
374        log.info("Activating consist {}", consistID);
375        active = true;
376            publish();
377    }
378
379    /**
380     * Deactivates and removes the consist from a throttle
381     */
382     public void deactivate() {
383
384        log.info("Deactivating consist {}", consistID);
385        active = false;
386    }
387
388    /**
389     * Publish the consist details to the controller
390     */
391    private void publish(){
392    }
393
394    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TmccConsist.class);
395
396}