001package jmri.jmrix.loconet; 002 003import java.util.Locale; 004import javax.annotation.Nonnull; 005import jmri.JmriException; 006import jmri.Sensor; 007import org.slf4j.Logger; 008import org.slf4j.LoggerFactory; 009import java.time.Instant; 010 011/** 012 * Manage the LocoNet-specific Sensor implementation. 013 * System names are "LSnnn", where L is the user configurable system prefix, 014 * nnn is the sensor number without padding. Valid sensor numbers are in the 015 * range 1 to 2048, inclusive. 016 * 017 * Provides a mechanism to perform the LocoNet "Interrogate" process in order 018 * to get initial values from those LocoNet devices which support the process 019 * and provide LocoNet Sensor (and/or LocoNet Turnout) functionality. 020 * 021 * @author Bob Jacobsen Copyright (C) 2001 022 * @author B. Milhaupt Copyright (C) 2020 023 */ 024public class LnSensorManager extends jmri.managers.AbstractSensorManager implements LocoNetListener { 025 026 protected final LnTrafficController tc; 027 028 /** 029 * Minimum amount of time since previous LocoNet Sensor State report or 030 * previous LocoNet Turnout State report or previous LocoNet "interrogate" 031 * message. 032 */ 033 protected int restingTime; 034 035 /** 036 * Instant at which last LocoNet Sensor State message or LocoNet Turnout state 037 * message or LocoNet Interrogation request message was received. 038 * 039 */ 040 private volatile Instant lastSensTurnInterrog; 041 042 public LnSensorManager(LocoNetSystemConnectionMemo memo, boolean interrogateAtStart) { 043 super(memo); 044 this.restingTime = 1250; 045 tc = memo.getLnTrafficController(); 046 if (tc == null) { 047 log.error("SensorManager Created, yet there is no Traffic Controller"); 048 return; 049 } 050 lastSensTurnInterrog = Instant.now(); // a baseline starting-point for 051 // interrogation timing 052 053 // ctor has to register for LocoNet events 054 tc.addLocoNetListener(~0, this); 055 056 if (interrogateAtStart) { 057 // start the update sequence. Until JMRI 2.9.4, this waited 058 // until files have been read, but starts automatically 059 // since 2.9.5 for multi-system support. 060 updateAll(); 061 } 062 } 063 064 /** {@inheritDoc} */ 065 @Override 066 @Nonnull 067 public LocoNetSystemConnectionMemo getMemo() { 068 return (LocoNetSystemConnectionMemo) memo; 069 } 070 071 // to free resources when no longer used 072 /** {@inheritDoc} */ 073 @Override 074 public void dispose() { 075 tc.removeLocoNetListener(~0, this); 076 Thread t = thread; 077 if (t != null) { 078 try { 079 t.interrupt(); 080 t.join(); 081 } catch (InterruptedException ex) { 082 log.warn("dispose interrupted"); 083 } finally { 084 thread = null; 085 } 086 } 087 super.dispose(); 088 } 089 090 // LocoNet-specific methods 091 092 /** {@inheritDoc} */ 093 @Override 094 @Nonnull 095 protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException { 096 return new LnSensor(systemName, userName, tc, getSystemPrefix()); 097 } 098 099 /** 100 * Listen for sensor messages, creating them as needed. 101 * @param l LocoNet message to be examined 102 */ 103 @Override 104 public void message(LocoNetMessage l) { 105 // parse message type 106 LnSensorAddress a; 107 LnSensor ns; 108 switch (l.getOpCode()) { 109 case LnConstants.OPC_INPUT_REP: /* page 9 of LocoNet PE */ 110 111 int sw1 = l.getElement(1); 112 int sw2 = l.getElement(2); 113 a = new LnSensorAddress(sw1, sw2, getSystemPrefix()); 114 log.debug("INPUT_REP received with address {}", a); 115 lastSensTurnInterrog = Instant.now(); 116 break; 117 case LnConstants.OPC_SW_REP: 118 lastSensTurnInterrog = Instant.now(); 119 return; 120 case LnConstants.OPC_SW_REQ: 121 case LnConstants.OPC_SW_ACK: 122 int address = ((l.getElement(1)& 0x7f) + 128*(l.getElement(2) & 0x0f)); 123 switch (address) { 124 case 0x3F8: 125 case 0x3F9: 126 case 0x3FA: 127 case 0x3FB: 128 lastSensTurnInterrog = Instant.now(); 129 return; 130 default: 131 break; 132 } 133 //$FALL-THROUGH$ 134 default: // here we didn't find an interesting command 135 return; 136 } 137 // reach here for LocoNet sensor input command; make sure we know about this one 138 String s = a.getNumericAddress(); 139 ns = (LnSensor) getBySystemName(s); 140 if (ns == null) { 141 // need to store a new one 142 if (log.isDebugEnabled()) { 143 log.debug("Create new LnSensor as {}", s); 144 } 145 ns = (LnSensor) newSensor(s, null); 146 } 147 ns.messageFromManager(l); // have it update state 148 } 149 150 volatile LnSensorUpdateThread thread; 151 152 /** 153 * Requests status updates from all layout sensors. 154 */ 155 @Override 156 public void updateAll() { 157 if (!busy) { 158 setUpdateBusy(); 159 thread = new LnSensorUpdateThread(this, tc, getRestingTime()); 160 thread.setName("LnSensorUpdateThread"); // NOI18N 161 thread.start(); 162 } 163 } 164 165 /** 166 * Set Route busy when commands are being issued to Route turnouts. 167 */ 168 public void setUpdateBusy() { 169 busy = true; 170 } 171 172 /** 173 * Set Route not busy when all commands have been issued to Route 174 * turnouts. 175 */ 176 public void setUpdateNotBusy() { 177 busy = false; 178 } 179 180 private boolean busy = false; 181 182 /** {@inheritDoc} */ 183 @Override 184 public boolean allowMultipleAdditions(@Nonnull String systemName) { 185 return true; 186 } 187 188 /** {@inheritDoc} */ 189 @Override 190 @Nonnull 191 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 192 if (curAddress.contains(":")) { // NOI18N 193 194 // NOTE: This format is deprecated in JMRI 4.17.4 on account the 195 // "byte:bit" format cannot be used under normal JMRI usage 196 // circumstances. It is retained for the normal deprecation period 197 // in order to support any atypical usage patterns. 198 199 int board = 0; 200 int channel = 0; 201 // Address format passed is in the form of board:channel or T:turnout address 202 int seperator = curAddress.indexOf(":"); // NOI18N 203 boolean turnout = false; 204 if (curAddress.substring(0, seperator).toUpperCase().equals("T")) { // NOI18N 205 turnout = true; 206 } else { 207 try { 208 board = Integer.parseInt(curAddress.substring(0, seperator)); 209 } catch (NumberFormatException ex) { 210 throw new JmriException("Unable to convert '"+curAddress+"' into the cab and channel format of nn:xx"); // NOI18N 211 } 212 } 213 try { 214 channel = Integer.parseInt(curAddress.substring(seperator + 1)); 215 } catch (NumberFormatException ex) { 216 throw new JmriException("Unable to convert '"+curAddress+"' into the cab and channel format of nn:xx"); // NOI18N 217 } 218 if (turnout) { 219 iName = 2 * (channel - 1) + 1; 220 } else { 221 iName = 16 * board + channel - 16; 222 } 223 jmri.util.LoggingUtil.warnOnce(log, 224 "LnSensorManager.createSystemName(curAddress, prefix) support for curAddress using the '{}' format is deprecated as of JMRI 4.17.4 and will be removed in a future JMRI release. Use the curAddress format '{}' instead.", // NOI18N 225 curAddress, iName); 226 } else { 227 // Entered in using the old format 228 log.debug("LnSensorManager creating system name for {}", curAddress); 229 try { 230 iName = Integer.parseInt(curAddress); 231 } catch (NumberFormatException ex) { 232 throw new JmriException("Hardware Address passed "+curAddress+" should be a number"); // NOI18N 233 } 234 } 235 return prefix + typeLetter() + iName; 236 } 237 238 int iName; 239 240 /** {@inheritDoc} */ 241 @Override 242 public NameValidity validSystemNameFormat(@Nonnull String systemName) { 243 return (getBitFromSystemName(systemName) != 0) ? NameValidity.VALID : NameValidity.INVALID; 244 } 245 246 /** {@inheritDoc} */ 247 @Override 248 @Nonnull 249 public String validateSystemNameFormat(@Nonnull String systemName, @Nonnull Locale locale) { 250 return validateIntegerSystemNameFormat(systemName, 1, 4096, locale); 251 } 252 253 /** 254 * Get the bit address from the system name. 255 * @param systemName a valid LocoNet-based Turnout System Name 256 * @return the turnout number extracted from the system name 257 */ 258 public int getBitFromSystemName(String systemName) { 259 try { 260 validateSystemNameFormat(systemName, Locale.getDefault()); 261 } catch (IllegalArgumentException ex) { 262 return 0; 263 } 264 return Integer.parseInt(systemName.substring(getSystemNamePrefix().length())); 265 } 266 267 /** {@inheritDoc} */ 268 @Override 269 public String getEntryToolTip() { 270 return Bundle.getMessage("AddInputEntryToolTip"); 271 } 272 273 /** 274 * Set "resting time" for the Interrogation process. 275 * 276 * A minimum of 500 (milliseconds) and a maximum of 200000 (milliseconds) is 277 * implemented. Values below this lower limit are forced to the lower 278 * limit, and values above this upper limit are forced to the upper limit. 279 * 280 * @param rest Number of milliseconds (minimum) before sending next 281 * LocoNet Interrogate message. 282 */ 283 public void setRestingTime(int rest) { 284 if (rest < 500) { 285 rest = 500; 286 } else if (rest > 200000) { 287 rest = 200000; 288 } 289 restingTime = rest; 290 } 291 292 /** 293 * get Interrogation process's "resting time" 294 * @return Resting time, in milliseconds 295 */ 296 public int getRestingTime() { 297 return restingTime; 298 } 299 300 /** 301 * Class providing a thread to query LocoNet Sensor and Turnout states. 302 */ 303 class LnSensorUpdateThread extends Thread { 304 private LnSensorManager sm = null; 305 private LnTrafficController tc = null; 306 private java.time.Duration restingTime; 307 308 /** 309 * Constructs the thread 310 * @param sm SensorManager to use 311 * @param tc TrafficController to use 312 * @param restingTime Min time before next LN query message sent 313 */ 314 public LnSensorUpdateThread(LnSensorManager sm, LnTrafficController tc, 315 int restingTime) { 316 this.sm = sm; 317 this.tc = tc; 318 this.restingTime = java.time.Duration.ofMillis(restingTime); 319 } 320 321 /** 322 * Runs the thread - sends 8 commands to query status of all stationary 323 * sensors (per LocoNet PE Specs, page 12-13). 324 * 325 * Timing between query messages is determined by certain previous LocoNet 326 * traffic (as noted by lastSensTurnInterrog) and restingTime. 327 */ 328 @Override 329 public void run() { 330 sm.setUpdateBusy(); 331 while (!tc.status()) { 332 try { 333 // Delay 500 mSec to allow init of traffic controller, 334 // listeners, and to limit amount of LocoNet traffic at 335 // JMRI start-up. 336 Thread.sleep(500); 337 } catch (InterruptedException e) { 338 Thread.currentThread().interrupt(); // retain if needed later 339 sm.setUpdateNotBusy(); 340 return; // and stop work 341 } 342 } 343 byte sw1[] = {0x78, 0x79, 0x7a, 0x7b, 0x78, 0x79, 0x7a, 0x7b}; 344 byte sw2[] = {0x27, 0x27, 0x27, 0x27, 0x07, 0x07, 0x07, 0x07}; 345 // create and initialize LocoNet message 346 LocoNetMessage msg = new LocoNetMessage(4); 347 msg.setOpCode(LnConstants.OPC_SW_REQ); 348 for (int k = 0; k < 8; k++) { 349 Instant n = Instant.now(); 350 Instant n2 = lastSensTurnInterrog.plus(restingTime); 351 int result = n.compareTo(n2); 352 log.debug("Interrogation phase {}: now {}, lastSensInterrog {}, target{}, time compare result {}", 353 k, n, lastSensTurnInterrog, n2, result); 354 while (result < 0) { 355 try { 356 // Delay 100 mSec 357 Thread.sleep(100); 358 } catch (InterruptedException e) { 359 Thread.currentThread().interrupt(); // retain if needed later 360 sm.setUpdateNotBusy(); 361 return; // and stop work 362 } 363 n = Instant.now(); 364 result = n.compareTo(n2); 365 log.debug("Interrogation phase {}: now {}, lastSensInterrog {}, target{}, time compare result {}", 366 k, n, lastSensTurnInterrog, n2, result); 367 } 368 msg.setElement(1, sw1[k]); 369 msg.setElement(2, sw2[k]); 370 371 tc.sendLocoNetMessage(msg); 372 373 /* lastSensTurnInterrog needs to be updated here to prevent quick 374 * sending of the next query message. (It will be updated upon 375 * reception of the LocoNet "echo".) 376 */ 377 lastSensTurnInterrog = Instant.now(); 378 379 log.debug("LnSensorUpdate sent"); 380 } 381 sm.setUpdateNotBusy(); 382 } 383 } 384 385 private final static Logger log = LoggerFactory.getLogger(LnSensorManager.class); 386 387}