001package jmri.implementation; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.List; 006import javax.annotation.CheckForNull; 007import javax.annotation.Nonnull; 008import jmri.NamedBeanHandle; 009import jmri.Turnout; 010import jmri.util.ThreadingUtil; 011import org.slf4j.Logger; 012import org.slf4j.LoggerFactory; 013 014/** 015 * SignalMast implemented via a Binary Matrix (Truth Table) of Apects x Turnout objects. 016 * <p> 017 * A MatrixSignalMast is built up from an array of turnouts to control each aspect. 018 * System name specifies the creation information (except for the actual output beans): 019 * <pre> 020 * IF$xsm:basic:one-searchlight:($0001)-3t 021 * </pre> The name is a colon-separated series of terms: 022 * <ul> 023 * <li>IF$xsm - defines signal masts of this type (x for matri<b>X</b>) 024 * <li>basic - name of the signaling system 025 * <li>one-searchlight - name of the particular aspect map/mast model 026 * <li>($0001) - small ordinal number for telling various matrix signal masts apart 027 * <li>name ending in -nt for (binary) Turnout outputs 028 * where n = the number of binary outputs, between 1 and mastBitNum i.e. -3t</li> 029 * </ul> 030 * 031 * @author Bob Jacobsen Copyright (C) 2009, 2014, 2020 032 * @author Egbert Broerse Copyright (C) 2016, 2018, 2020 033 */ 034public class MatrixSignalMast extends AbstractSignalMast { 035 /** 036 * Number of columns in logix matrix, default to 6, set in Matrix Mast panel & on loading xml. 037 * Used to set size of char[] bitString. 038 * See MAXMATRIXBITS in {@link jmri.jmrit.beantable.signalmast.MatrixSignalMastAddPane}. 039 */ 040 private int mastBitNum = 6; 041 private int mDelay = 0; 042 043 private static final String errorChars = "nnnnnn"; 044 private final char[] errorBits = errorChars.toCharArray(); 045 046 private static final String emptyChars = "000000"; // default starting value 047 private final char[] emptyBits = emptyChars.toCharArray(); 048 049 public MatrixSignalMast(String systemName, String userName) { 050 super(systemName, userName); 051 configureFromName(systemName); 052 } 053 054 public MatrixSignalMast(String systemName) { 055 super(systemName); 056 configureFromName(systemName); 057 } 058 059 private static final String THE_MAST_TYPE = "IF$xsm"; 060 061 private void configureFromName(@Nonnull String systemName) { 062 // split out the basic information 063 String[] parts = systemName.split(":"); 064 if (parts.length < 3) { 065 log.error("SignalMast system name needs at least three parts: {}", systemName); 066 throw new IllegalArgumentException("System name needs at least three parts: " + systemName); 067 } 068 if (!parts[0].equals(THE_MAST_TYPE)) { 069 log.warn("SignalMast system name should start with \"{}\" but is \"{}\"", THE_MAST_TYPE, systemName); 070 } 071 String system = parts[1]; 072 String mast = parts[2]; 073 074 mast = mast.substring(0, mast.indexOf("(")); 075 setMastType(mast); 076 077 String tmp = parts[2].substring(parts[2].indexOf("($") + 2, parts[2].indexOf(")")); // retrieve ordinal from name 078 try { 079 int autoNumber = Integer.parseInt(tmp); 080 if (autoNumber > getLastRef()) { 081 setLastRef(autoNumber); 082 } 083 } catch (NumberFormatException e) { 084 log.warn("Auto generated SystemName \"{}\" is not in the correct format", systemName); 085 } 086 087 configureSignalSystemDefinition(system); // (checks for system) in AbstractSignalMast 088 configureAspectTable(system, mast); // (create -default- appmapping in var "map") in AbstractSignalMast 089 } 090 091 private final HashMap<String, char[]> aspectToOutput = new HashMap<>(16); // "Clear" - 01001 char[] pairs 092 private char[] unLitBits; 093 094 /** 095 * Store bits in aspectToOutput hashmap, synchronized. 096 * <p> 097 * Length of bitArray should match the number of outputs defined, so one digit per output. 098 * 099 * @param aspect String valid aspect to define 100 * @param bitArray char[] of on/off outputs for the aspect, like "00010" 101 */ 102 public synchronized void setBitsForAspect(String aspect, char[] bitArray) { 103 if (aspectToOutput.containsKey(aspect)) { 104 if (log.isDebugEnabled()) log.debug("Aspect {} is already defined as {}", aspect, java.util.Arrays.toString(aspectToOutput.get(aspect))); 105 aspectToOutput.remove(aspect); 106 } 107 aspectToOutput.put(aspect, bitArray); // store keypair aspectname - bitArray in hashmap 108 } 109 110 /** 111 * Look up the pattern for an aspect. 112 * 113 * @param aspect String describing a (valid) signal mast aspect, like "Clear" 114 * only called for an already existing mast 115 * @return char[] of on/off outputs per aspect, like "00010" 116 * length of array should match the number of outputs defined 117 * when a mast is changed in the interface, extra 0's are added or superfluous elements deleted by the Add Mast panel 118 */ 119 public synchronized char[] getBitsForAspect(String aspect) { 120 if (!aspectToOutput.containsKey(aspect) || aspectToOutput.get(aspect) == null) { 121 log.error("Trying to get aspect {} but it has not been configured", aspect); 122 return errorBits; // error flag 123 } 124 return aspectToOutput.get(aspect); 125 } 126 127 @Override 128 public void setAspect(@Nonnull String aspect) { 129 // check it's a valid choice 130 if (!map.checkAspect(aspect)) { 131 // not a valid aspect 132 log.warn("attempting to set invalid Aspect: {} on mast {}", aspect, getDisplayName()); 133 throw new IllegalArgumentException("attempting to set invalid Aspect: " + aspect + " on mast: " + getDisplayName()); 134 } else if (disabledAspects.contains(aspect)) { 135 log.warn("attempting to set an Aspect that has been Disabled: {} on mast {}", aspect, getDisplayName()); 136 throw new IllegalArgumentException("attempting to set an Aspect that has been Disabled: " + aspect + " on mast: " + getDisplayName()); 137 } 138 if (getLit()) { 139 synchronized (this) { 140 // If the signalmast is lit, then send the commands to change the aspect. 141 if (resetPreviousStates) { 142 // Clear all the current states, this will result in the signalmast going "Stop" or unLit for a while 143 if (aspectToOutput.containsKey("Stop")) { 144 updateOutputs(getBitsForAspect("Stop")); // show Red 145 } else { 146 if (unLitBits != null) { 147 updateOutputs(unLitBits); // Dark (instead of Red), always available 148 } 149 } 150 } 151 // add a timer here to wait a while before setting new aspect? 152 if (aspectToOutput.containsKey(aspect) && aspectToOutput.get(aspect) != errorBits) { 153 char[] bitArray = getBitsForAspect(aspect); 154 // for MatrixMast nest a loop, using setBitsForAspect(), provides extra check on value 155 updateOutputs(bitArray); 156 // Set the new Signal Mast state 157 } else { 158 log.error("Trying to set an aspect ({}) on signal mast {} which has not been configured", aspect, getDisplayName()); 159 } 160 } 161 } else { 162 log.debug("Mast set to unlit, will not send aspect change to hardware"); 163 } 164 super.setAspect(aspect); 165 } 166 167 @Override 168 public void setLit(boolean newLit) { 169 if (!allowUnLit() || newLit == getLit()) { 170 return; 171 } 172 super.setLit(newLit); 173 if (newLit) { 174 String litAspect = getAspect(); 175 if (litAspect != null) { 176 setAspect(litAspect); 177 } 178 // if true, activate prior aspect 179 } else { 180 if (unLitBits != null) { 181 updateOutputs(unLitBits); // directly set outputs 182 //c.sendPacket(NmraPacket.altAccSignalDecoderPkt(dccSignalDecoderAddress, unLitId), packetRepeatCount); 183 } 184 } 185 } 186 187 public void setUnLitBits(@Nonnull char[] bits) { 188 unLitBits = bits; 189 } 190 191 /** 192 * Receive unLitBits from xml and store. 193 * 194 * @param bitString String for 1-n 1/0 chararacters setting an unlit aspect 195 */ 196 public void setUnLitBits(@Nonnull String bitString) { 197 setUnLitBits(bitString.toCharArray()); 198 } 199 200 /** 201 * Provide Unlit bits to panel for editing. 202 * 203 * @return char[] containing a series of 1's and 0's set for Unlit mast 204 */ 205 @Nonnull public char[] getUnLitBits() { 206 if (unLitBits != null) { 207 return unLitBits; 208 } else { 209 return emptyBits; 210 } 211 } 212 213 /** 214 * Hand unLitBits to xml. 215 * 216 * @return String for 1-n 1/0 chararacters setting an unlit aspect 217 */ 218 @Nonnull public String getUnLitChars() { 219 if (unLitBits != null) { 220 return String.valueOf(unLitBits); 221 } else { 222 log.error("Returning 0 values because unLitBits is empty"); 223 return emptyChars.substring(0, (mastBitNum)); // should only be called when Unlit = true 224 } 225 } 226 227 /** 228 * Fetch output as Turnout from outputsToBeans hashmap. 229 * 230 * @param colNum int index (1 up to mastBitNum) for the column of the desired output 231 * @return Turnout object connected to configured output 232 */ 233 @CheckForNull private Turnout getOutputBean(int colNum) { // as bean 234 String key = "output" + colNum; 235 if (colNum > 0 && colNum <= outputsToBeans.size()) { 236 return outputsToBeans.get(key).getBean(); 237 } 238 log.error("Trying to read bean for output {} which has not been configured", colNum); 239 return null; 240 } 241 242 /** 243 * Fetch output from outputsToBeans hashmap. 244 * Used? 245 * 246 * @param colNum int index (1 up to mastBitNum) for the column of the desired output 247 * @return NamedBeanHandle to the configured turnout output 248 */ 249 @CheckForNull public NamedBeanHandle<Turnout> getOutputHandle(int colNum) { 250 String key = "output" + colNum; 251 if (colNum > 0 && colNum <= outputsToBeans.size()) { 252 return outputsToBeans.get(key); 253 } 254 log.error("Trying to read output NamedBeanHandle {} which has not been configured", key); 255 return null; 256 } 257 258 /** 259 * Fetch output from outputsToBeans hashmap and provide to xml. 260 * 261 * @see jmri.implementation.configurexml.MatrixSignalMastXml#store(java.lang.Object) 262 * @param colnum int index (1 up to mastBitNum) for the column of the desired output 263 * @return String with the desplay name of the configured turnout output 264 */ 265 @Nonnull public String getOutputName(int colnum) { 266 String key = "output" + colnum; 267 if (colnum > 0 && colnum <= outputsToBeans.size()) { 268 return outputsToBeans.get(key).getName(); 269 } 270 log.error("Trying to read name of output {} which has not been configured", colnum); 271 return ""; 272 } 273 274 /** 275 * Receive aspect name from xml and store matching setting in outputsToBeans hashmap. 276 * 277 * @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element) 278 * @param aspect String describing (valid) signal mast aspect, like "Clear" 279 * @param bitString String of 1/0 digits representing on/off outputs per aspect, like "00010" 280 */ 281 public synchronized void setBitstring(@Nonnull String aspect, @Nonnull String bitString) { 282 if (aspectToOutput.containsKey(aspect)) { 283 log.debug("Aspect {} is already defined so will override", aspect); 284 aspectToOutput.remove(aspect); 285 } 286 char[] bitArray = bitString.toCharArray(); // for faster lookup, stored as char[] array 287 aspectToOutput.put(aspect, bitArray); 288 } 289 290 /** 291 * Receive aspect name from xml and store matching setting in outputsToBeans hashmap. 292 * 293 * @param aspect String describing (valid) signal mast aspect, like "Clear" 294 * @param bitArray char[] of 1/0 digits representing on/off outputs per aspect, like {0,0,0,1,0} 295 */ 296 public synchronized void setBitstring(String aspect, char[] bitArray) { 297 if (aspectToOutput.containsKey(aspect)) { 298 log.debug("Aspect {} is already defined so will override", aspect); 299 aspectToOutput.remove(aspect); 300 } 301 // is supplied as char array, no conversion needed 302 aspectToOutput.put(aspect, bitArray); 303 } 304 305 /** 306 * Provide one series of on/off digits from aspectToOutput hashmap to xml. 307 * 308 * @return bitString String of 1 (= on) and 0 (= off) chars 309 * @param aspect String describing valid signal mast aspect, like "Clear" 310 */ 311 @Nonnull public synchronized String getBitstring(@Nonnull String aspect) { 312 if (aspectToOutput.containsKey(aspect)) { // hashtable 313 return new String(aspectToOutput.get(aspect)); // convert char[] to string 314 } 315 return ""; 316 } 317 318 /** 319 * Provide the names of the on/off turnout outputs from outputsToBeans hashmap to xml. 320 * 321 * @return outputlist List<String> of display names for the outputs in order 1 to (max) mastBitNum 322 */ 323 @Nonnull public List<String> getOutputs() { // provide to xml 324 // to do: use for loop 325 ArrayList<String> outputlist = new ArrayList<>(); 326 //list = outputsToBeans.keySet(); 327 328 int index = 1; 329 while (outputsToBeans.containsKey("output" + index)) { 330 outputlist.add(outputsToBeans.get("output" + index).getName()); 331 index++; 332 } 333 return outputlist; 334 } 335 336 protected HashMap<String, NamedBeanHandle<Turnout>> outputsToBeans = new HashMap<>(); // output# - bean pairs 337 338 /** 339 * Receive properties from xml, convert name to NamedBeanHandle, store in hashmap outputsToBeans. 340 * 341 * @param colname String describing the name of the corresponding output, like "output1" 342 * @param turnoutname String for the display name of the output, like "LT1" 343 */ 344 public void setOutput(@Nonnull String colname, @Nonnull String turnoutname) { 345 Turnout turn = jmri.InstanceManager.turnoutManagerInstance().getTurnout(turnoutname); 346 if (turn == null) { 347 log.error("setOutput couldn't locate turnout {}", turnoutname); 348 return; 349 } 350 NamedBeanHandle<Turnout> namedTurnout = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(turnoutname, turn); 351 if (outputsToBeans.containsKey(colname)) { 352 log.debug("Output {} is already defined so will override", colname); 353 outputsToBeans.remove(colname); 354 } 355 outputsToBeans.put(colname, namedTurnout); 356 } 357 358 /** 359 * Send hardware instruction. 360 * 361 * @param bits char[] of on/off outputs per aspect, like "00010" 362 * Length of array should match the number of outputs defined 363 */ 364 public void updateOutputs(char[] bits) { 365 int newState; 366 if (bits == null){ 367 log.debug("Empty char[] received"); 368 } else { 369 for (int i = 0; i < outputsToBeans.size(); i++) { 370 log.debug("Setting bits[1] = {} for output #{}", bits[i], i); 371 Turnout t = getOutputBean(i + 1); 372 if (t != null) { 373 t.setBinaryOutput(true); // prevent feedback etc. 374 } 375 if (bits[i] == '1' && t != null && t.getCommandedState() != Turnout.CLOSED) { 376 // no need to set a state already set 377 newState = Turnout.CLOSED; 378 } else if (bits[i] == '0' && t != null && t.getCommandedState() != Turnout.THROWN) { 379 newState = Turnout.THROWN; 380 } else if (bits[i] == 'n' || bits[i] == 'u') { 381 // let pass, extra chars up to mastBitNum are not defined 382 newState = -1; 383 } else { 384 // invalid char or state is already set 385 newState = -2; 386 log.debug("Element {} not converted to state for output #{}", bits[i], i); 387 } 388 // wait mast specific delay before sending each (valid) state change to a (valid) output 389 if (newState >= 0 && t != null) { // t!=null check required 390 final int toState = newState; 391 final Turnout setTurnout = t; 392 ThreadingUtil.runOnLayoutEventually(() -> { // eventually, even though we have timing here, should be soon 393 setTurnout.setCommandedStateAtInterval(toState); // delayed on specific connection by its turnoutManager 394 }); 395 try { 396 Thread.sleep(mDelay); // only the Mast specific user defined delay is applied here 397 } catch (InterruptedException e) { 398 log.debug("interrupted in updateOutputs"); 399 Thread.currentThread().interrupt(); // retain if needed later 400 return; 401 } 402 } 403 } 404 } 405 } 406 407 private boolean resetPreviousStates = false; 408 409 /** 410 * If the signal mast driver requires the previous state to be cleared down 411 * before the next state is set. 412 * 413 * @param boo true to configure for intermediate reset step 414 */ 415 public void resetPreviousStates(boolean boo) { 416 resetPreviousStates = boo; 417 } 418 419 public boolean resetPreviousStates() { 420 return resetPreviousStates; 421 } 422 423/* Turnout getTurnoutBean(int i) { // as bean 424 String key = "output" + Integer.toString(i); 425 if (i < 1 || i > outputsToBeans.size() ) { 426 return null; 427 } 428 if (outputsToBeans.containsKey(key) && outputsToBeans.get(key) != null){ 429 return outputsToBeans.get(key).getBean(); 430 } 431 return null; 432 }*/ 433 434/* public String getTurnoutName(int i) { 435 String key = "output" + Integer.toString(i); 436 if (i < 1 || i > outputsToBeans.size() ) { 437 return null; 438 } 439 if (outputsToBeans.containsKey(key) && outputsToBeans.get(key) != null) { 440 return outputsToBeans.get(key).getName(); 441 } 442 return null; 443 }*/ 444 445 public boolean isTurnoutUsed(Turnout t) { 446 for (int i = 1; i <= outputsToBeans.size(); i++) { 447 if (t.equals(getOutputBean(i))) { 448 return true; 449 } 450 } 451 return false; 452 } 453 454 /** 455 * @return highest ordinal of all MatrixSignalMasts in use 456 */ 457 public static int getLastRef() { 458 return lastRef; 459 } 460 461 /** 462 * 463 * @param newVal for ordinal of all MatrixSignalMasts in use 464 */ 465 protected static void setLastRef(int newVal) { 466 lastRef = newVal; 467 } 468 469 /** 470 * Ordinal of all MatrixSignalMasts to create unique system name. 471 */ 472 private static volatile int lastRef = 0; 473 474 @Override 475 public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException { 476 if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N 477 if (evt.getOldValue() instanceof Turnout) { 478 if (isTurnoutUsed((Turnout) evt.getOldValue())) { 479 java.beans.PropertyChangeEvent e = new java.beans.PropertyChangeEvent(this, "DoNotDelete", null, null); 480 throw new java.beans.PropertyVetoException(Bundle.getMessage("InUseTurnoutSignalMastVeto", getDisplayName()), e); 481 } 482 } 483 } 484 } 485 486 /** 487 * Store number of outputs from integer. 488 * 489 * @param number int for the number of outputs defined for this mast 490 * @see #mastBitNum 491 */ 492 public void setBitNum(int number) { 493 mastBitNum = number; 494 } 495 496 /** 497 * Store number of outputs from integer. 498 * 499 * @param bits char[] for outputs defined for this mast 500 * @see #mastBitNum 501 */ 502 public void setBitNum(char[] bits) { 503 mastBitNum = bits.length; 504 } 505 506 public int getBitNum() { 507 return mastBitNum; 508 } 509 510 @Override 511 public void setAspectDisabled(String aspect) { 512 if (aspect == null || aspect.equals("")) { 513 return; 514 } 515 if (!map.checkAspect(aspect)) { 516 log.warn("attempting to disable an aspect: {} that is not on mast {}", aspect, getDisplayName()); 517 return; 518 } 519 if (!disabledAspects.contains(aspect)) { 520 disabledAspects.add(aspect); 521 firePropertyChange("aspectDisabled", null, aspect); 522 } 523 } 524 525 /** 526 * Set the delay between issuing Matrix Output commands to the outputs on this specific mast. 527 * Mast Delay will be extended by a connection specific Output Delay set in the connection config. 528 * 529 * @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element) 530 * @param delay the new delay in milliseconds 531 */ 532 public void setMatrixMastCommandDelay(int delay) { 533 if (delay >= 0) { 534 mDelay = delay; 535 } 536 } 537 538 /** 539 * Get the delay between issuing Matrix Output commands to the outputs on this specific mast. 540 * Delay be extended by a connection specific Output Delay set in the connection config. 541 * 542 * @see jmri.implementation.configurexml.MatrixSignalMastXml#load(org.jdom2.Element, org.jdom2.Element) 543 * @return the delay in milliseconds 544 */ 545 public int getMatrixMastCommandDelay() { 546 return mDelay; 547 } 548 549 private final static Logger log = LoggerFactory.getLogger(MatrixSignalMast.class); 550 551}