001package jmri.jmrix.nce; 002 003import java.util.Locale; 004import javax.annotation.Nonnull; 005import jmri.JmriException; 006import jmri.NamedBean; 007import jmri.Sensor; 008import jmri.jmrix.AbstractMRReply; 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012 013/** 014 * Manage the NCE-specific Sensor implementation. 015 * <p> 016 * System names are "NSnnn", where N is the user configurable system prefix, 017 * nnn is the sensor number without padding. 018 * <p> 019 * This class is responsible for generating polling messages for the 020 * NceTrafficController, see nextAiuPoll() 021 * 022 * @author Bob Jacobsen Copyright (C) 2003 023 * @author Ken Cameron (C) 2023 024 */ 025public class NceSensorManager extends jmri.managers.AbstractSensorManager 026 implements NceListener { 027 028 public NceSensorManager(NceSystemConnectionMemo memo) { 029 super(memo); 030 aiuCabIdMin = memo.getNceTrafficController().csm.getCabMin(); 031 aiuCabIdMax = memo.getNceTrafficController().csm.getCabMax(); 032 aiuArray = new NceAIU[aiuCabIdMax + 1]; // element 0 isn't used 033 for (int i = aiuCabIdMin; i <= aiuCabIdMax; i++) { 034 aiuArray[i] = null; 035 } 036 activeAIUs = new int[aiuCabIdMax]; // keep track of those worth polling 037 mInstance = this; 038 listener = new NceListener() { 039 @Override 040 public void message(NceMessage m) { 041 } 042 043 @Override 044 public void reply(NceReply r) { 045 if (r.isSensorMessage()) { 046 mInstance.handleSensorMessage(r); 047 } 048 } 049 }; 050 memo.getNceTrafficController().addNceListener(listener); 051 } 052 053 private final NceSensorManager mInstance; 054 private final int aiuCabIdMin; 055 private final int aiuCabIdMax; 056 private NceAIU[] aiuArray = null; // P 057 private int[] activeAIUs = null; // P 058 private int activeAIUMax = 0; // last+1 element used of activeAIUs P 059 private static final int MAXPIN = 14; // only pins 1 - 14 used on NCE AIU 060 061 /** 062 * {@inheritDoc} 063 */ 064 @Override 065 @Nonnull 066 public NceSystemConnectionMemo getMemo() { 067 return (NceSystemConnectionMemo) memo; 068 } 069 070 // to free resources when no longer used 071 @Override 072 public void dispose() { 073 stopPolling = true; // tell polling thread to go away 074 Thread thread = pollThread; 075 if (thread != null) { 076 try { 077 thread.interrupt(); 078 thread.join(); 079 } catch (InterruptedException ex) { 080 log.warn("dispose interrupted"); 081 } 082 } 083 getMemo().getNceTrafficController().removeNceListener(listener); 084 super.dispose(); 085 } 086 087 /** 088 * {@inheritDoc} 089 * <p> 090 * Assumes calling method has checked that a Sensor with this system 091 * name does not already exist. 092 * 093 * @throws IllegalArgumentException if the system name is not in a valid format 094 */ 095 @Override 096 @Nonnull 097 protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException { 098 099 int number = 0; 100 String normName; 101 try { 102 // see if this is a valid address 103 String address = systemName.substring(getSystemPrefix().length() + 1); 104 normName = createSystemName(address, getSystemPrefix()); 105 // parse converted system name 106 number = Integer.parseInt(normName.substring(getSystemPrefix().length() + 1)); 107 } catch (NumberFormatException | JmriException e) { 108 throw new IllegalArgumentException("Unable to convert " + // NOI18N 109 systemName.substring(getSystemPrefix().length() + 1) + 110 " to NCE sensor address"); // NOI18N 111 } 112 Sensor s = new NceSensor(normName); 113 s.setUserName(userName); 114 115 // ensure the AIU exists 116 int index = (number / 16) + 1; 117 if (aiuArray[index] == null) { 118 aiuArray[index] = new NceAIU(); 119 buildActiveAIUs(); 120 } 121 122 // register this sensor with the AIU 123 aiuArray[index].registerSensor(s, number - (index - 1) * 16); 124 125 return s; 126 } 127 128 volatile Thread pollThread; 129 volatile boolean stopPolling = false; 130 NceListener listener; 131 132 // polling parameters and variables 133 private boolean loggedAiuNotSupported = false; // set after logging that AIU isn't supported on this config 134 private final int shortCycleInterval = 200; 135 private final int longCycleInterval = 10000; // when we know async messages are flowing 136 private final long maxSilentInterval = 30000; // max slow poll time without hearing an async message 137 private final int pollTimeout = 20000; // in case of lost response 138 private int aiuCycleCount; 139 private long lastMessageReceived; // time of last async message 140 private NceAIU currentAIU; 141 private boolean awaitingReply = false; 142 private boolean awaitingDelay = false; 143 144 /** 145 * Build the array of the indices of AIUs which have been polled, and 146 * ensures that pollManager has all the information it needs to work 147 * correctly. 148 * 149 */ 150 /* Some logic notes 151 * 152 * Sensor polling normally happens on a short cycle - the NCE round-trip 153 * response time (normally 50mS, set by the serial line timeout) plus 154 * the "shortCycleInterval" defined above. If an async sensor message is received, 155 * we switch to the longCycleInterval since really we don't need to poll at all. 156 * 157 * We use the long poll only if the following conditions are satisified: 158 * 159 * -- there have been at least two poll cycle completions since the last change 160 * to the list of active sensor - this means at least one complete poll cycle, 161 * so we are sure we know the states of all the sensors to begin with 162 * 163 * -- we have received an async message in the last maxSilentInterval, so that 164 * if the user turns off async messages (possible, though dumb in mid session) 165 * the system will stumble back to life 166 * 167 * The interaction between buildActiveAIUs and pollManager is designed so that 168 * no explicit sync or locking is needed when the former changes the list of active 169 * AIUs used by the latter. At worst, there will be one cycle which polls the same 170 * sensor twice. 171 * 172 * Be VERY CAREFUL if you change any of this. 173 * 174 */ 175 private void buildActiveAIUs() { 176 if ((getMemo().getNceTrafficController().getCmdGroups() & NceTrafficController.CMDS_AUI_READ) 177 != NceTrafficController.CMDS_AUI_READ) { 178 if (!loggedAiuNotSupported) { 179 log.info("AIU not supported in this configuration"); 180 loggedAiuNotSupported = true; 181 return; 182 } 183 } 184 activeAIUMax = 0; 185 for (int a = aiuCabIdMin; a <= aiuCabIdMax; ++a) { 186 if (aiuArray[a] != null) { 187 activeAIUs[activeAIUMax++] = a; 188 } 189 } 190 aiuCycleCount = 0; // force another polling cycle 191 lastMessageReceived = Long.MIN_VALUE; 192 if (activeAIUMax > 0) { 193 if (pollThread == null) { 194 pollThread = new Thread(new Runnable() { 195 @Override 196 public void run() { 197 pollManager(); 198 } 199 }); 200 pollThread.setName(getMemo().getNceTrafficController().getUserName()+" Sensor Poll"); 201 pollThread.setDaemon(true); 202 pollThread.start(); 203 } else { 204 synchronized (this) { 205 if (awaitingDelay) { // interrupt long between-poll wait 206 notify(); 207 } 208 } 209 } 210 } 211 } 212 213 public NceMessage makeAIUPoll(int aiuNo) { 214 if (getMemo().getNceTrafficController().getUsbSystem() == NceTrafficController.USB_SYSTEM_NONE) { 215 // use old 4 byte read command if not USB 216 return makeAIUPoll4ByteReply(aiuNo); 217 } else { 218 // use new 2 byte read command if USB 219 return makeAIUPoll2ByteReply(aiuNo); 220 } 221 } 222 223 /** 224 * Construct a binary-formatted AIU poll message 225 * 226 * @param aiuNo number of AIU to poll 227 * @return message to be queued 228 */ 229 private NceMessage makeAIUPoll4ByteReply(int aiuNo) { 230 NceMessage m = new NceMessage(2); 231 m.setBinary(true); 232 m.setReplyLen(NceMessage.REPLY_4); 233 m.setElement(0, NceMessage.READ_AUI4_CMD); 234 m.setElement(1, aiuNo); 235 m.setTimeout(pollTimeout); 236 return m; 237 } 238 239 /** 240 * construct a binary-formatted AIU poll message 241 * 242 * @param aiuNo number of AIU to poll 243 * @return message to be queued 244 */ 245 private NceMessage makeAIUPoll2ByteReply(int aiuNo) { 246 NceMessage m = new NceMessage(2); 247 m.setBinary(true); 248 m.setReplyLen(NceMessage.REPLY_2); 249 m.setElement(0, NceMessage.READ_AUI2_CMD); 250 m.setElement(1, aiuNo); 251 m.setTimeout(pollTimeout); 252 return m; 253 } 254 255 /** 256 * Send poll messages for AIU sensors. Also interact with 257 * asynchronous sensor state messages. Adjust poll cycle according to 258 * whether any async messages have been received recently. Also we require 259 * one poll of each sensor before squelching active polls. 260 */ 261 private void pollManager() { 262 if ((getMemo().getNceTrafficController().getCmdGroups() & NceTrafficController.CMDS_AUI_READ) 263 != NceTrafficController.CMDS_AUI_READ) { 264 if (!loggedAiuNotSupported) { 265 log.info("AIU not supported in this configuration"); 266 loggedAiuNotSupported = true; 267 } 268 } else { 269 while (!stopPolling) { 270 for (int a = 0; a < activeAIUMax; ++a) { 271 int aiuNo = activeAIUs[a]; 272 currentAIU = aiuArray[aiuNo]; 273 if (currentAIU != null) { // in case it has gone away 274 NceMessage m = makeAIUPoll(aiuNo); 275 synchronized (this) { 276 log.debug("queueing poll request for AIU {}", aiuNo); 277 getMemo().getNceTrafficController().sendNceMessage(m, this); 278 awaitingReply = true; 279 try { 280 wait(pollTimeout); 281 } catch (InterruptedException e) { 282 Thread.currentThread().interrupt(); // retain if needed later 283 return; 284 } 285 } 286 int delay = shortCycleInterval; 287 if (aiuCycleCount >= 2 288 && lastMessageReceived >= System.currentTimeMillis() - maxSilentInterval) { 289 delay = longCycleInterval; 290 } 291 synchronized (this) { 292 if (awaitingReply && !stopPolling) { 293 log.warn("timeout awaiting poll response for AIU {}", aiuNo); 294 // slow down the poll since we're not getting responses 295 // this lets NceConnectionStatus to do its thing 296 delay = pollTimeout; 297 } 298 try { 299 awaitingDelay = true; 300 wait(delay); 301 } catch (InterruptedException e) { 302 Thread.currentThread().interrupt(); // retain if needed later 303 return; 304 } finally { 305 awaitingDelay = false; 306 } 307 } 308 } 309 } 310 ++aiuCycleCount; 311 } 312 } 313 } 314 315 @Override 316 public void message(NceMessage r) { 317 log.warn("unexpected message"); 318 } 319 320 /** 321 * Process single received reply from sensor poll. 322 */ 323 @Override 324 public void reply(NceReply r) { 325 if (!r.isUnsolicited()) { 326 int bits; 327 synchronized (this) { 328 bits = r.pollValue(); // bits is the value in hex from the message 329 awaitingReply = false; 330 this.notify(); 331 } 332 currentAIU.markChanges(bits); 333 if (log.isDebugEnabled()) { 334 String str = jmri.util.StringUtil.twoHexFromInt((bits >> 4) & 0xf); 335 str += " "; 336 str = jmri.util.StringUtil.appendTwoHexFromInt(bits & 0xf, str); 337 log.debug("sensor poll reply received: \"{}\"", str); 338 } 339 } 340 } 341 342 /** 343 * Handle an unsolicited sensor (AIU) state message. 344 * 345 * @param r sensor message 346 */ 347 public void handleSensorMessage(AbstractMRReply r) { 348 int index = r.getElement(1) - 0x30; 349 int indicator = r.getElement(2); 350 if (r.getElement(0) == 0x61 && r.getElement(1) >= 0x30 && r.getElement(1) <= 0x6f 351 && ((indicator >= 0x41 && indicator <= 0x5e) || (indicator >= 0x61 && indicator <= 0x7e))) { 352 lastMessageReceived = System.currentTimeMillis(); 353 if (aiuArray[index] == null) { 354 log.debug("unsolicited message \"{}\" for unused sensor array", r.toString()); 355 } else { 356 int sensorNo; 357 int newState; 358 if (indicator >= 0x60) { 359 sensorNo = indicator - 0x61; 360 newState = Sensor.ACTIVE; 361 } else { 362 sensorNo = indicator - 0x41; 363 newState = Sensor.INACTIVE; 364 } 365 Sensor s = aiuArray[index].getSensor(sensorNo); 366 if (s.getInverted()) { 367 if (newState == Sensor.ACTIVE) { 368 newState = Sensor.INACTIVE; 369 } else if (newState == Sensor.INACTIVE) { 370 newState = Sensor.ACTIVE; 371 } 372 } 373 374 if (log.isDebugEnabled()) { 375 log.debug("Handling sensor message \"{}\" for {} {}", 376 r, s.getSystemName(), s.describeState(newState) ); 377 } 378 aiuArray[index].sensorChange(sensorNo, newState); 379 } 380 } else { 381 log.warn("incorrect sensor message: {}", r.toString()); 382 } 383 } 384 385 @Override 386 public boolean allowMultipleAdditions(@Nonnull String systemName) { 387 return true; 388 } 389 390 @Override 391 @Nonnull 392 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 393 if (curAddress.contains(":")) { 394 // Sensor address is presented in the format AIU Cab Address:Pin Number On AIU 395 // Should we be validating the values of aiucab address and pin number? 396 // Yes we should, added check for valid AIU and pin ranges DBoudreau 2/13/2013 397 int seperator = curAddress.indexOf(":"); 398 try { 399 aiucab = Integer.parseInt(curAddress.substring(0, seperator)); 400 pin = Integer.parseInt(curAddress.substring(seperator + 1)); 401 } catch (NumberFormatException ex) { 402 throw new JmriException("Unable to convert "+curAddress+" into the cab and pin format of nn:xx"); 403 } 404 iName = (aiucab - 1) * 16 + pin - 1; 405 406 } else { 407 //Entered in using the old format 408 try { 409 iName = Integer.parseInt(curAddress); 410 } catch (NumberFormatException ex) { 411 throw new JmriException("Hardware Address passed "+curAddress+" should be a number or the cab and pin format of nn:xx"); 412 } 413 pin = iName % 16 + 1; 414 aiucab = iName / 16 + 1; 415 } 416 // only pins 1 through 14 are valid 417 if (pin == 0 || pin > MAXPIN) { 418 throw new JmriException("Sensor pin number "+pin+" for address "+curAddress+" is out of range; only pin numbers 1 - 14 are valid"); 419 } 420 if (aiucab < aiuCabIdMin || aiucab > aiuCabIdMax) { 421 throw new JmriException("AIU number "+aiucab+" for address "+curAddress+" is out of range; only AIU "+aiuCabIdMin+" - "+aiuCabIdMax+" are valid"); 422 } 423 return prefix + typeLetter() + iName; 424 } 425 426 int aiucab = 0; 427 int pin = 0; 428 int iName = 0; 429 430 /** 431 * {@inheritDoc} 432 */ 433 @Override 434 @Nonnull 435 public String validateSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) { 436 String parts[]; 437 int num; 438 if (name.contains(":")) { 439 parts = super.validateSystemNameFormat(name, locale) 440 .substring(getSystemNamePrefix().length()).split(":"); 441 if (parts.length != 2) { 442 throw new NamedBean.BadSystemNameException( 443 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNeedCabAndPin", name), 444 Bundle.getMessage(locale, "InvalidSystemNameNeedCabAndPin", name)); 445 } 446 } else { 447 parts = new String[]{"0", "0"}; 448 try { 449 num = Integer.parseInt(super.validateSystemNameFormat(name, locale) 450 .substring(getSystemNamePrefix().length())); 451 parts[0] = Integer.toString((num / 16) + 1); // aiu cab 452 parts[1] = Integer.toString((num % 16) + 1); // aiu pin 453 } catch (NumberFormatException ex) { 454 throw new NamedBean.BadSystemNameException( 455 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNeedCabAndPin", name), 456 Bundle.getMessage(locale, "InvalidSystemNameNeedCabAndPin", name)); 457 } 458 } 459 try { 460 num = Integer.parseInt(parts[0]); 461 if (num < aiuCabIdMin || num > aiuCabIdMax) { 462 throw new NamedBean.BadSystemNameException( 463 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUCab", name, aiuCabIdMin, aiuCabIdMax), 464 Bundle.getMessage(locale, "InvalidSystemNameBadAIUCab", name, aiuCabIdMin, aiuCabIdMax)); 465 } 466 } catch (NumberFormatException ex) { 467 throw new NamedBean.BadSystemNameException( 468 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUCab", name, aiuCabIdMin, aiuCabIdMax), 469 Bundle.getMessage(locale, "InvalidSystemNameBadAIUCab", name, aiuCabIdMin, aiuCabIdMax)); 470 } 471 try { 472 num = Integer.parseInt(parts[1]); 473 if (num < 1 || num > MAXPIN) { 474 throw new NamedBean.BadSystemNameException( 475 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUPin", name), 476 Bundle.getMessage(locale, "InvalidSystemNameBadAIUPin", name)); 477 } 478 } catch (NumberFormatException ex) { 479 throw new NamedBean.BadSystemNameException( 480 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBadAIUCab", name, aiuCabIdMin, aiuCabIdMax), 481 Bundle.getMessage(locale, "InvalidSystemNameBadAIUCab", name, aiuCabIdMin, aiuCabIdMax)); 482 } 483 return name; 484 } 485 486 /** 487 * {@inheritDoc} 488 */ 489 @Override 490 public NameValidity validSystemNameFormat(@Nonnull String systemName) { 491 if (super.validSystemNameFormat(systemName) == NameValidity.VALID) { 492 try { 493 validateSystemNameFormat(systemName); 494 } catch (IllegalArgumentException ex) { 495 if (systemName.endsWith(":")) { 496 try { 497 int num = Integer.parseInt(systemName.substring(getSystemNamePrefix().length(), systemName.length() - 1)); 498 if (num >= aiuCabIdMin && num <= aiuCabIdMax) { 499 return NameValidity.VALID_AS_PREFIX_ONLY; 500 } 501 } catch (NumberFormatException | IndexOutOfBoundsException iex) { 502 // do nothing; will return INVALID 503 } 504 } 505 return NameValidity.INVALID; 506 } 507 } 508 return NameValidity.VALID; 509 } 510 511 /** 512 * {@inheritDoc} 513 */ 514 @Override 515 public String getEntryToolTip() { 516 return Bundle.getMessage("AddInputEntryToolTip"); 517 } 518 519 private final static Logger log = LoggerFactory.getLogger(NceSensorManager.class); 520 521}