001package jmri.jmrix.loconet;
002
003import jmri.InstanceManager;
004import jmri.Programmer;
005import jmri.ProgrammingMode;
006import jmri.beans.PropertyChangeSupport;
007import jmri.jmrit.decoderdefn.DecoderFile;
008import jmri.jmrit.decoderdefn.DecoderIndexFile;
009import jmri.jmrit.roster.Roster;
010import jmri.jmrit.roster.RosterEntry;
011import jmri.jmrix.ProgrammingTool;
012import jmri.jmrix.loconet.lnsvf1.Lnsv1Device;
013import jmri.jmrix.loconet.lnsvf1.Lnsv1Devices;
014import jmri.jmrix.loconet.lnsvf1.Lnsv1MessageContents;
015import jmri.managers.DefaultProgrammerManager;
016//import jmri.progdebugger.ProgDebugger;
017
018import jmri.util.ThreadingUtil;
019import jmri.util.swing.JmriJOptionPane;
020
021import javax.annotation.concurrent.GuardedBy;
022import java.util.List;
023
024/**
025 * LocoNet LNSV1 Devices Manager
026 * <p>
027 * A centralized resource to help identify LocoNet "LNSV1 Format"
028 * devices and "manage" them.
029 * <p>
030 * Supports the following features:
031 *  - LNSV1 "discovery" process supported via BROADCAST call
032 *  - LNSV1 Device "destination address" change supported by writing a new value to LNSV 0 (close session next)
033 *  - LNSV1 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 LNSVf1 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, 2025
044 */
045
046public class Lnsv1DevicesManager extends PropertyChangeSupport
047        implements LocoNetListener {
048    private final LocoNetSystemConnectionMemo memo;
049    @GuardedBy("this")
050    private final Lnsv1Devices lnsv1Devices;
051
052    public Lnsv1DevicesManager(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            lnsv1Devices = new Lnsv1Devices();
061        }
062    }
063
064    public synchronized Lnsv1Devices getDeviceList() {
065        return lnsv1Devices;
066    }
067
068    public synchronized int getDeviceCount() {
069        return lnsv1Devices.size();
070    }
071
072    public void clearDevicesList() {
073        synchronized (this) {
074            lnsv1Devices.removeAllDevices();
075        }
076        jmri.util.ThreadingUtil.runOnLayoutEventually( ()-> firePropertyChange("DeviceListChanged", true, false));
077    }
078
079    /**
080     * Extract module information from a LNSVf1 READ_ONE REPLY message.
081     * If not already in the lnsv1Devices list, try to find a matching decoder definition (by address number and programming mode)
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    @Override
089    public void message(LocoNetMessage m) {
090        if (Lnsv1MessageContents.isSupportedSv1Message(m)) {
091            if ((Lnsv1MessageContents.extractMessageType(m) == Lnsv1MessageContents.Sv1Command.SV1_READ) &&
092                    (Lnsv1MessageContents.extractMessageVersion(m) > 0)) { // which marks replies from devices
093                // it's an LNSV1 Read_One Reply message, decode contents:
094                Lnsv1MessageContents contents = new Lnsv1MessageContents(m);
095                int vrs = contents.getVersionNum();
096                int addrL = contents.getSrcL();
097                int subAddr = contents.getSubAddress();
098                int sv = contents.getSvNum();
099                int val = contents.getSvValue();
100                log.debug("Lnsv1DevicesManager got read reply: vrs:{}, address:{}/{} cv:{} val:{}", vrs, addrL, subAddr, sv, val);
101
102                synchronized (this) {
103                    if (lnsv1Devices.addDevice(new Lnsv1Device(addrL, subAddr, sv, val, "", "", vrs))) {
104                        log.debug("new Lnsv1Device added to table");
105                        // Annotate the discovered device LNSV1 data based on address
106                        for (int i = 0; i < lnsv1Devices.size(); ++i) { // find the added item
107                            Lnsv1Device dev = lnsv1Devices.getDevice(i);
108                            if ((dev.getDestAddrLow() == addrL) && (dev.getDestAddrHigh() == subAddr)) {
109                                // Try to find a roster entry which matches the device characteristics
110                                log.debug("Looking for adr {} in Roster", dev.getDestAddr());
111
112                                // threadUtil off GUI for Roster reading decoderfiles cf. LncvDevicesManager
113                                ThreadingUtil.newThread(() -> {
114                                    List<RosterEntry> rl;
115                                    try {
116                                        // requires nonnull default for jmri.jmrit.roster.RosterConfigManager
117                                        rl = Roster.getDefault().getEntriesMatchingCriteria(
118                                                Integer.toString(dev.getDestAddr()), // composite DCC address
119                                                null, null, null,
120                                                null); // TODO filter on progMode LNSV1 only on new roster entries
121                                        log.debug("Lnsv1DeviceManager found {} matches in Roster", rl.size());
122                                        if (rl.isEmpty()) {
123                                            log.debug("No corresponding roster entry found");
124                                        } else if (rl.size() == 1) {
125                                            log.debug("Matching roster entry found");
126                                            dev.setRosterEntry(rl.get(0)); // link this device to the entry
127                                            String title = rl.get(0).getDecoderModel() + " (" + rl.get(0).getDecoderFamily() + ")";
128                                            // fileFromTitle() matches by model + " (" + family + ")"
129                                            DecoderFile decoderFile = InstanceManager.getDefault(DecoderIndexFile.class).fileFromTitle(title);
130                                            if (decoderFile != null) {
131                                                // TODO check for LNSV1 mode
132                                                dev.setDecoderFile(decoderFile); // link to decoderFile (to check programming mode from table)
133                                                log.debug("Attached a decoderfile");
134                                            } else {
135                                                log.warn("Could not attach decoderfile {} to entry", rl.get(0).getFileName());
136                                            }
137                                        } else { // matches > 1
138                                            JmriJOptionPane.showMessageDialog(null,
139                                                    Bundle.getMessage("WarnMultipleLnsv1ModsFound", rl.size(), addrL, subAddr),
140                                                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
141                                            log.info("Found multiple matching LNSV1 roster entries. " + "Cannot associate any one to this device.");
142                                        }
143
144                                    } catch (Exception e) {
145                                        log.error("Error creating Roster.matchingList: {}", e.getMessage());
146                                    }
147                                }, "rosterMatchingList").start();
148                                // this will block until the thread completes, either by finishing or by being cancelled
149
150                                // notify listeners of pertinent change to device list
151                                firePropertyChange("DeviceListChanged", true, false);
152                            }
153                        }
154                    } else {
155                        log.debug("LNSV1 device was already in list");
156                    }
157                }
158            } else {
159                log.debug("LNSV1 message not a READ REPLY [{}]", m);
160            }
161        } else {
162            log.debug("LNSV1 message not recognized");
163        }
164    }
165
166    public synchronized Lnsv1Device getDevice(int vrs, int addr) {
167        for (int i = 0; i < lnsv1Devices.size(); ++ i) {
168            Lnsv1Device dev = lnsv1Devices.getDevice(i);
169            if ((dev.getSwVersion() == vrs) && (dev.getDestAddr() == addr)) {
170                return dev;
171            }
172        }
173        return null;
174    }
175
176    public ProgrammingResult prepareForSymbolicProgrammer(Lnsv1Device dev, ProgrammingTool t) {
177        synchronized(this) {
178            if (lnsv1Devices.isDeviceExistant(dev) < 0) {
179                return ProgrammingResult.FAIL_NO_SUCH_DEVICE;
180            }
181            int destAddr = dev.getDestAddr();
182            if (destAddr == 0) {
183                return ProgrammingResult.FAIL_DESTINATION_ADDRESS_IS_ZERO;
184            }
185            int deviceCount = 0;
186            for (Lnsv1Device d : lnsv1Devices.getDevices()) {
187                if (destAddr == d.getDestAddr()) {
188                    deviceCount++;
189                }
190            }
191            log.debug("prepareForSymbolicProgrammer found {} matches", deviceCount);
192            if (deviceCount > 1) {
193                return ProgrammingResult.FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS;
194            }
195        }
196
197        if ((dev.getRosterName() == null) || (dev.getRosterName().isEmpty())) {
198            return ProgrammingResult.FAIL_NO_MATCHING_ROSTER_ENTRY;
199        }
200
201        // check if roster entry still present in Roster
202        RosterEntry re = Roster.getDefault().entryFromTitle(dev.getRosterName());
203        if (re == null) {
204            log.warn("Could not open LNSV1 Programmer because {} not found in Roster. Removed from device",
205                    dev.getRosterName());
206            dev.setRosterEntry(null);
207            jmri.util.ThreadingUtil.runOnLayoutEventually( ()-> firePropertyChange("DeviceListChanged", true, false));
208            return ProgrammingResult.FAIL_NO_MATCHING_ROSTER_ENTRY;
209        }
210        String name = re.getId();
211
212        DefaultProgrammerManager pm = memo.getProgrammerManager();
213        if (pm == null) {
214            return ProgrammingResult.FAIL_NO_APPROPRIATE_PROGRAMMER;
215        }
216        Programmer p = pm.getAddressedProgrammer(false, dev.getDestAddr());
217        if (p == null) {
218            return ProgrammingResult.FAIL_NO_ADDRESSED_PROGRAMMER;
219        }
220
221        //if (p.getClass() != ProgDebugger.class) { // Debug in Simulator
222            // ProgDebugger is used for LocoNet HexFile Sim; uncommenting above line allows testing of LNSV1 Tool
223            if (!p.getSupportedModes().contains(LnProgrammerManager.LOCONETOPSBOARD)) {
224                return ProgrammingResult.FAIL_NO_LNSV1_PROGRAMMER;
225            }
226            p.setMode(LnProgrammerManager.LOCONETSV1MODE);
227            ProgrammingMode prgMode = p.getMode();
228            if (!prgMode.equals(LnProgrammerManager.LOCONETSV1MODE)) {
229                return ProgrammingResult.FAIL_NO_LNSV1_PROGRAMMER;
230            }
231        //}
232
233        t.openPaneOpsProgFrame(re, name, "programmers/Comprehensive.xml", p); // NOI18N
234        return ProgrammingResult.SUCCESS_PROGRAMMER_OPENED;
235    }
236
237    public enum ProgrammingResult {
238        SUCCESS_PROGRAMMER_OPENED,
239        FAIL_NO_SUCH_DEVICE,
240        FAIL_NO_APPROPRIATE_PROGRAMMER,
241        FAIL_NO_MATCHING_ROSTER_ENTRY,
242        FAIL_DESTINATION_ADDRESS_IS_ZERO,
243        FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS,
244        FAIL_NO_ADDRESSED_PROGRAMMER,
245        FAIL_NO_LNSV1_PROGRAMMER
246    }
247
248    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Lnsv1DevicesManager.class);
249
250}