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}