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