001package jmri.jmrix.loconet;
002
003import java.util.List;
004
005import javax.annotation.concurrent.GuardedBy;
006
007import jmri.InstanceManager;
008import jmri.Programmer;
009import jmri.ProgrammingMode;
010import jmri.beans.PropertyChangeSupport;
011import jmri.jmrit.decoderdefn.DecoderFile;
012import jmri.jmrit.decoderdefn.DecoderIndexFile;
013import jmri.jmrit.roster.Roster;
014import jmri.jmrit.roster.RosterEntry;
015
016import jmri.jmrix.ProgrammingTool;
017import jmri.jmrix.loconet.uhlenbrock.LncvMessageContents;
018import jmri.jmrix.loconet.uhlenbrock.LncvDevice;
019import jmri.jmrix.loconet.uhlenbrock.LncvDevices;
020import jmri.managers.DefaultProgrammerManager;
021//import jmri.progdebugger.ProgDebugger;
022
023import jmri.util.ThreadingUtil;
024import jmri.util.swing.JmriJOptionPane;
025
026/**
027 * LocoNet LNCV Devices Manager
028 * <p>
029 * A centralized resource to help identify LocoNet "LNCV Format"
030 * devices and "manage" them.
031 * <p>
032 * Supports the following features:
033 *  - LNCV "discovery" process supported via PROG_START_ALL call
034 *  - LNCV Device "destination address" change supported by writing a new value to LNCV 0 (close session next)
035 *  - LNCV Device "reconfigure/reset" not supported/documented
036 *  - identification of devices with conflicting "destination address"es (warning before program start)
037 *  - identification of a matching JMRI "decoder definition" for each discovered
038 *    device, if an appropriate definition exists (only 1 value is matched, checks for LNCV protocol support)
039 *  - identification of matching JMRI "roster entry" which matches each
040 *    discovered device, if an appropriate roster entry exists
041 *  - ability to open a symbolic programmer for a given discovered device, if
042 *    an appropriate roster entry exists
043 *
044 * @author B. Milhaupt Copyright (c) 2020
045 * @author Egbert Broerse (c) 2021
046 */
047
048public class LncvDevicesManager extends PropertyChangeSupport
049        implements LocoNetListener {
050    private final LocoNetSystemConnectionMemo memo;
051    @GuardedBy("this")
052    private final LncvDevices lncvDevices;
053
054    public LncvDevicesManager(LocoNetSystemConnectionMemo memo) {
055        this.memo = memo;
056        if (memo.getLnTrafficController() != null) {
057            memo.getLnTrafficController().addLocoNetListener(~0, this);
058        } else {
059            log.error("No LocoNet connection available, this tool cannot function"); // NOI18N
060        }
061        synchronized (this) {
062            lncvDevices = new LncvDevices();
063        }
064    }
065
066    public synchronized LncvDevices getDeviceList() {
067        return lncvDevices;
068    }
069
070    public synchronized int getDeviceCount() {
071        return lncvDevices.size();
072    }
073
074    public void clearDevicesList() {
075        synchronized (this) {
076            lncvDevices.removeAllDevices();
077        }
078        jmri.util.ThreadingUtil.runOnLayoutEventually( ()-> firePropertyChange("DeviceListChanged", true, false));
079    }
080
081    /**
082     * Extract module information from LNCV READREPLY/READREPLY2 message,
083     * if not already in the lncvDevices list, try to find a matching decoder definition (by article number)
084     * and add it. Skip if already in the list.
085     *
086     * @param m The received LocoNet message. Note that this same object may
087     *            be presented to multiple users. It should not be modified
088     *            here.
089     */
090    @Override
091    public void message(LocoNetMessage m) {
092        if (LncvMessageContents.isSupportedLncvMessage(m)) {
093            if ((LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY) ||
094                    //Updated 2022 to also accept undocumented Digikeijs DR5088 reply format LNCV_READ_REPLY2
095                    (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY2)) {
096                // it's an LNCV ReadReply message, decode contents:
097                LncvMessageContents contents = new LncvMessageContents(m);
098                int art = contents.getLncvArticleNum();
099                int addr = -1;
100                int cv = contents.getCvNum();
101                int val = contents.getCvValue();
102                log.debug("LncvDevicesManager got read reply: art:{}, address:{} cv:{} val:{}", art, addr, cv, val);
103                if (cv == 0) { // trust last used address
104                    addr = val; // if cvNum = 0, this is the LNCV module address
105                    log.debug("LNCV read reply: device address {} of LNCV returns {}", addr, val);
106
107                    synchronized (this) {
108                        if (lncvDevices.addDevice(new LncvDevice(art, addr, cv, val, "", "", -1))) {
109                            log.debug("new LncvDevice added to table");
110                            // Annotate the discovered device LNCV data based on address
111                            for (int i = 0; i < lncvDevices.size(); ++i) {
112                                LncvDevice dev = lncvDevices.getDevice(i);
113                                if ((dev.getProductID() == art) && (dev.getDestAddr() == addr)) {
114                                    // need to find a corresponding roster entry
115                                    if (dev.getRosterName() != null && dev.getRosterName().isEmpty()) {
116                                        // Yes. Try to find a roster entry which matches the device characteristics
117                                        log.debug("Looking for prodID {}/adr {} in Roster", dev.getProductID(), dev.getDestAddr());
118
119                                        // Try to find a roster entry which matches the device characteristics
120                                        log.debug("Looking for adr {} in Roster", dev.getDestAddr());
121
122                                        // threadUtil off GUI for Roster reading decoderfiles cf. Lnsv1DevicesManager
123                                        ThreadingUtil.newThread(() -> {
124                                            List<RosterEntry> rl;
125                                            try {
126                                                rl = Roster.getDefault().getEntriesMatchingCriteria(
127                                                        Integer.toString(dev.getDestAddr()), // composite DCC address
128                                                        null, null, Integer.toString(dev.getProductID()),
129                                                        null); // TODO filter on progMode LNCV only on new roster entries
130                                                log.debug("LncvDeviceManager found {} matches in Roster", rl.size());
131                                                if (rl.isEmpty()) {
132                                                    log.debug("No corresponding RosterEntry found");
133                                                } else if (rl.size() == 1) {
134                                                    log.debug("Matching RosterEntry found");
135                                                    dev.setRosterEntry(rl.get(0)); // link this device to the entry
136
137                                                    String title = rl.get(0).getDecoderModel() + " (" + rl.get(0).getDecoderFamily() + ")";
138                                                    // fileFromTitle() matches by model + " (" + family + ")"
139                                                    DecoderFile decoderFile = InstanceManager.getDefault(DecoderIndexFile.class).fileFromTitle(title);
140                                                    if (decoderFile != null) {
141                                                        // TODO check for LNCV mode
142                                                        dev.setDecoderFile(decoderFile); // link to decoderFile (to check programming mode from table)
143                                                        log.debug("Attached a decoderfile");
144                                                    } else {
145                                                        log.warn("Could not attach decoderfile {} to entry", rl.get(0).getFileName());
146                                                    }
147
148                                                } else { // matches > 1
149                                                    JmriJOptionPane.showMessageDialog(null,
150                                                            Bundle.getMessage("WarnMultipleLncvModsFound", art, dev.getDestAddr(), rl.size()),
151                                                            Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
152                                                    log.info("Found multiple matching roster entries. " + "Cannot associate any one to this device.");
153                                                }
154                                            } catch (Exception e) {
155                                                log.error("Error creating Roster.matchingList: {}", e.getMessage());
156                                            }
157                                        }, "rosterMatchingList").start();
158                                        // this will block until the thread completes, either by finishing or by being cancelled
159
160
161                                    }
162                                    // notify listeners of pertinent change to device list
163                                    firePropertyChange("DeviceListChanged", true, false);
164                                }
165                            }
166                        } else {
167                            log.debug("LNCV device was already in list");
168                        }
169                    }
170                } else {
171                    log.debug("LNCV device check skipped as value not CV0/module address");
172                }
173            } else {
174                log.debug("LNCV message not a READ REPLY [{}]", m);
175            }
176        } else {
177            log.debug("LNCV message not recognized");
178        }
179    }
180
181    public synchronized LncvDevice getDevice(int art, int addr) {
182        for (int i = 0; i < lncvDevices.size(); ++ i) {
183            LncvDevice dev = lncvDevices.getDevice(i);
184            if ((dev.getProductID() == art) && (dev.getDestAddr() == addr)) {
185                return dev;
186            }
187        }
188        return null;
189    }
190
191    public ProgrammingResult prepareForSymbolicProgrammer(LncvDevice dev, ProgrammingTool t) {
192        synchronized(this) {
193            if (lncvDevices.isDeviceExistant(dev) < 0) {
194                return ProgrammingResult.FAIL_NO_SUCH_DEVICE;
195            }
196            int destAddr = dev.getDestAddr();
197            if (destAddr == 0) {
198                return ProgrammingResult.FAIL_DESTINATION_ADDRESS_IS_ZERO;
199            }
200            int deviceCount = 0;
201            for (LncvDevice d : lncvDevices.getDevices()) {
202                if (destAddr == d.getDestAddr()) {
203                    deviceCount++;
204                }
205            }
206            log.debug("prepareForSymbolicProgrammer found {} matches", deviceCount);
207            if (deviceCount > 1) {
208                return ProgrammingResult.FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS;
209            }
210        }
211
212        if ((dev.getRosterName() == null) || (dev.getRosterName().isEmpty())) {
213            return ProgrammingResult.FAIL_NO_MATCHING_ROSTER_ENTRY;
214        }
215
216        // check if roster entry still present in Roster
217        RosterEntry re = Roster.getDefault().entryFromTitle(dev.getRosterName());
218        if (re == null) {
219            log.warn("Could not open LNSV1 Programmer because {} not found in Roster. Removed from device",
220                    dev.getRosterName());
221            dev.setRosterEntry(null);
222            jmri.util.ThreadingUtil.runOnLayoutEventually( ()-> firePropertyChange("DeviceListChanged", true, false));
223            return ProgrammingResult.FAIL_NO_MATCHING_ROSTER_ENTRY;
224        }
225        String name = re.getId();
226
227        DefaultProgrammerManager pm = memo.getProgrammerManager();
228        if (pm == null) {
229            return ProgrammingResult.FAIL_NO_APPROPRIATE_PROGRAMMER;
230        }
231        Programmer p = pm.getAddressedProgrammer(false, dev.getDestAddr());
232        if (p == null) {
233            return ProgrammingResult.FAIL_NO_ADDRESSED_PROGRAMMER;
234        }
235
236        //if (p.getClass() != ProgDebugger.class) { // Debug in Simulator
237            // ProgDebugger is used for LocoNet HexFile Sim, uncommenting above line allows testing of LNCV Tool
238            if (!p.getSupportedModes().contains(LnProgrammerManager.LOCONETLNCVMODE)) {
239                return ProgrammingResult.FAIL_NO_LNCV_PROGRAMMER;
240            }
241            p.setMode(LnProgrammerManager.LOCONETLNCVMODE);
242            ProgrammingMode prgMode = p.getMode();
243            if (!prgMode.equals(LnProgrammerManager.LOCONETLNCVMODE)) {
244                return ProgrammingResult.FAIL_NO_LNCV_PROGRAMMER;
245            }
246        //}
247
248        t.openPaneOpsProgFrame(re, name, "programmers/Comprehensive.xml", p); // NOI18N
249        return ProgrammingResult.SUCCESS_PROGRAMMER_OPENED;
250    }
251
252    public enum ProgrammingResult {
253        SUCCESS_PROGRAMMER_OPENED,
254        FAIL_NO_SUCH_DEVICE,
255        FAIL_NO_APPROPRIATE_PROGRAMMER,
256        FAIL_NO_MATCHING_ROSTER_ENTRY,
257        FAIL_DESTINATION_ADDRESS_IS_ZERO,
258        FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS,
259        FAIL_NO_ADDRESSED_PROGRAMMER,
260        FAIL_NO_LNCV_PROGRAMMER
261    }
262
263    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LncvDevicesManager.class);
264
265}