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