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}