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