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            // Set range on TMCC1 Consist ID (1-15)
165            if (consistAddress.getNumber() < 16) {
166
167                // TMCC has 6 commands for adding a loco to a consist: head, rear, and mid, plus direction
168                if (!contains(locoAddress)) {
169                    // First loco to consist
170                    if (consistList.isEmpty()) {
171                        // add head loco
172                        if (!directionNormal) {
173                            // TMCC1 - Assign as Head Unit/Reverse Direction
174                            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
175                            m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
176                            n.putAsWord(0x0025 + locoAddress.getNumber() * 128);
177                        } else {
178                            // TMCC1 - Assign as Head Unit/Forward Direction
179                            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
180                            m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
181                            n.putAsWord(0x0021 + locoAddress.getNumber() * 128);
182                        }
183                    // send to command station (send once (each) is set, but number of sends may need to be adjusted depending on efficiency)
184                    tc.sendSerialMessage(c, null);
185                    tc.sendSerialMessage(m, null);
186                    tc.sendSerialMessage(n, null);
187
188                    // Second loco to consist
189                    } else if (consistList.size() == 1) {
190                        // add rear loco
191                        if (!directionNormal) {
192                            // TMCC1 - Assign as Rear Unit/Reverse Direction
193                            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
194                            m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
195                            n.putAsWord(0x0027 + locoAddress.getNumber() * 128);
196                        } else {
197                            // TMCC1 - Assign as Rear Unit/Forward Direction
198                            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
199                            m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
200                            n.putAsWord(0x0023 + locoAddress.getNumber() * 128);
201                        }
202                    // send to command station (send once (each) is set, but number of sends may need to be adjusted depending on efficiency)
203                    tc.sendSerialMessage(c, null);
204                    tc.sendSerialMessage(m, null);
205                    tc.sendSerialMessage(n, null);
206
207                    // Additional loco(s) to consist
208                    } else {
209                        // add mid loco
210                        if (!directionNormal) {
211                            // TMCC1 - Assign as Mid Unit/Reverse Direction
212                            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
213                            m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
214                            n.putAsWord(0x0026 + locoAddress.getNumber() * 128);
215                        } else {
216                            // TMCC1 - Assign as Mid Unit/Forward Direction
217                            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128)); // Clear residual consist ID from locomotive
218                            m.putAsWord(0x0030 + (locoAddress.getNumber() * 128) + consistAddress.getNumber());
219                            n.putAsWord(0x0022 + locoAddress.getNumber() * 128);
220                        }
221                    // send to command station (send once (each) is set, but number of sends may need to be adjusted depending on efficiency)
222                    tc.sendSerialMessage(c, null);
223                    tc.sendSerialMessage(m, null);
224                    tc.sendSerialMessage(n, null);
225
226                    }
227
228                    // Add Loco to Consist List
229                    addToConsistList(locoAddress, directionNormal);
230
231                } else {
232                    log.error("Loco {} is already part of this consist {}", locoAddress, getConsistAddress());
233                }
234
235            } else {
236                m.putAsWord(0x002F);
237                tc.sendSerialMessage(m, null);
238                log.error("TMCC1 Consist ID out of Range (must be between 1-15)");
239            }
240        }
241
242        // TMCC2 Consist Build
243        if (locoAddress.getProtocol() == LocoAddress.Protocol.TMCC2) {
244            SerialMessage c = new SerialMessage();
245            SerialMessage m = new SerialMessage();
246            SerialMessage n = new SerialMessage();
247            c.setOpCode(0xF8);
248            m.setOpCode(0xF8);
249            n.setOpCode(0xF8);
250
251            // Set range on TMCC2 Consist ID (1-15)
252            if (consistAddress.getNumber() < 16) {
253
254                // TMCC has 6 commands for adding a loco to a consist: head, rear, and mid, plus direction
255                if (!contains(locoAddress)) {
256
257                    // First loco to consist
258                    if (consistList.isEmpty()) {
259                        // add head loco
260                        if (!directionNormal) {
261                            // TMCC1 - Assign as Head Unit/Reverse Direction
262                            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
263                            m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
264                            n.putAsWord(0x0123 + locoAddress.getNumber() * 512);
265                        } else {
266                            // TMCC1 - Assign as Head Unit/Forward Direction
267                            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
268                            m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
269                            n.putAsWord(0x0122 + locoAddress.getNumber() * 512);
270                        }
271                    // send to command station (send once (each) is set, but number of sends may need to be adjusted depending on efficiency)
272                    tc.sendSerialMessage(c, null);
273                    tc.sendSerialMessage(m, null);
274                    tc.sendSerialMessage(n, null);
275
276                    // Second loco to consist
277                    } else if (consistList.size() == 1) {
278                        // add rear loco
279                        if (!directionNormal) {
280                            // TMCC1 - Assign as Rear Unit/Reverse Direction
281                            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
282                            m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
283                            n.putAsWord(0x0127 + locoAddress.getNumber() * 512);
284                        } else {
285                            // TMCC1 - Assign as Rear Unit/Forward Direction
286                            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
287                            m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
288                            n.putAsWord(0x0126 + locoAddress.getNumber() * 512);
289                        }
290                    // send to command station (send once (each) is set, but number of sends may need to be adjusted depending on efficiency)
291                    tc.sendSerialMessage(c, null);
292                    tc.sendSerialMessage(m, null);
293                    tc.sendSerialMessage(n, null);
294
295                    // Additional loco(s) to consist
296                    } else {
297                        // add mid loco
298                        if (!directionNormal) {
299                            // TMCC1 - Assign as Mid Unit/Reverse Direction
300                            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
301                            m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
302                            n.putAsWord(0x0125 + locoAddress.getNumber() * 512);
303                        } else {
304                            // TMCC1 - Assign as Mid Unit/Forward Direction
305                            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512)); // Clear residual consist ID from locomotive
306                            m.putAsWord(0x0130 + (locoAddress.getNumber() * 512) + consistAddress.getNumber());
307                            n.putAsWord(0x0124 + locoAddress.getNumber() * 512);
308                        }
309                    // send to command station (send once (each) is set, but number of sends may need to be adjusted depending on efficiency)
310                    tc.sendSerialMessage(c, null);
311                    tc.sendSerialMessage(m, null);
312                    tc.sendSerialMessage(n, null);
313
314                    }
315
316                    // Add Loco to Consist List
317                    addToConsistList(locoAddress, directionNormal);
318
319                } else {
320                    log.error("Loco {} is already part of this consist {}", locoAddress, getConsistAddress());
321                }
322
323            } else {
324                m.putAsWord(0x012F);
325                tc.sendSerialMessage(m, null);
326                log.error("TMCC2 Consist ID out of Range (must be between 1-15)");
327            }
328        }
329    }
330
331    /**
332     * Restore a Locomotive to Consist, but don't write to
333     * the command station.  This is used for restoring the consist
334     * from a file or adding a consist read from the command station.
335     *
336     * @param locoAddress is the Locomotive address to add to the locomotive
337     * @param directionNormal is True if the locomotive is traveling
338     *        the same direction as the consist, or false otherwise.
339     */
340    @Override
341    public synchronized void restore(DccLocoAddress locoAddress, boolean directionNormal) {
342        log.debug("Restore to consist address {} direction {}", locoAddress, directionNormal);
343
344        if (consistType == CS_CONSIST) {
345            addToConsistList(locoAddress, directionNormal);
346        } else {
347            log.error("Consist Type Not Supported");
348            notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
349        }
350    }
351
352    /**
353     * Remove a Locomotive from this Consist
354     * Clear Consist ID from Locomotive
355     * @param locoAddress is the Locomotive address to add to the locomotive
356     */
357    @Override
358    public synchronized void remove(DccLocoAddress locoAddress) {
359        log.debug("Remove from consist address {}", locoAddress);
360
361        // TMCC1 - Clear Consist ID from Locomotive
362        if (locoAddress.getProtocol() == LocoAddress.Protocol.TMCC1) {
363            SerialMessage c = new SerialMessage();
364            c.setOpCode(0xFE);
365            c.putAsWord(0x0030 + (locoAddress.getNumber() * 128));
366            tc.sendSerialMessage(c, null);
367        }
368
369        // TMCC2 - Clear Consist ID from Locomotive
370        if (locoAddress.getProtocol() == LocoAddress.Protocol.TMCC2) {
371            SerialMessage c = new SerialMessage();
372            c.setOpCode(0xF8);
373            c.putAsWord(0x0130 + (locoAddress.getNumber() * 512));
374            tc.sendSerialMessage(c, null);
375        }
376
377        // Remove Locomotive from this Consist
378        if (consistType == CS_CONSIST) {
379            removeFromConsistList(locoAddress);
380            if (active) {
381                publish();
382            }
383        } else {
384                log.error("Consist Type Not Supported");
385                notifyConsistListeners(locoAddress, ConsistListener.NotImplemented);
386        }
387    }
388
389    /**
390     * Activates the consist for use with a throttle
391     */
392    public void activate(){
393        log.info("Activating consist {}", consistID);
394        active = true;
395            publish();
396    }
397
398    /**
399     * Deactivates and removes the consist from a throttle
400     */
401     public void deactivate() {
402
403        log.info("Deactivating consist {}", consistID);
404        active = false;
405    }
406
407    /**
408     * Publish the consist details to the controller
409     */
410    private void publish(){
411    }
412
413    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TmccConsist.class);
414
415}