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