001package jmri.jmrix.loconet; 002 003import java.util.ArrayList; 004import java.util.List; 005import java.util.Locale; 006import javax.annotation.Nonnull; 007import jmri.BooleanPropertyDescriptor; 008import jmri.NamedBean; 009import jmri.NamedBeanPropertyDescriptor; 010import jmri.Turnout; 011import jmri.managers.AbstractTurnoutManager; 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015/** 016 * Manage the LocoNet-specific Turnout implementation. 017 * System names are "LTnnn", where L is the user configurable system prefix, 018 * nnn is the turnout number without padding. 019 * <p> 020 * Some of the message formats used in this class are Copyright Digitrax, Inc. 021 * and used with permission as part of the JMRI project. That permission does 022 * not extend to uses in other software products. If you wish to use this code, 023 * algorithm or these message formats outside of JMRI, please contact Digitrax 024 * Inc for separate permission. 025 * <p> 026 * Since LocoNet messages requesting turnout operations can arrive faster than 027 * the command station can send them on the rails, the command station has a 028 * short queue of messages. When that gets full, it sends a LACK, indicating 029 * that the request was not forwarded on the rails. In that case, this class 030 * goes into a tight loop, resending the last turnout message seen until it's 031 * received without a LACK reply. Note two things about this: 032 * <ul> 033 * <li>We provide this service for any turnout request, whether or not it came 034 * from JMRI. (This might be a problem if more than one computer is executing 035 * this algorithm) 036 * <li>By sending the message as fast as we can, we tie up the LocoNet during 037 * the recovery. This is a mixed bag; delaying can cause messages to get out 038 * of sequence on the rails. But not delaying takes up a lot of LocoNet 039 * bandwidth. 040 * </ul> 041 * In the end, this implementation is OK, but not great. An improvement would be 042 * to control JMRI turnout operations centrally, so that retransmissions can be 043 * controlled. 044 * 045 * @author Bob Jacobsen Copyright (C) 2001, 2007 046 */ 047public class LnTurnoutManager extends AbstractTurnoutManager implements LocoNetListener { 048 049 // ctor has to register for LocoNet events 050 public LnTurnoutManager(LocoNetSystemConnectionMemo memo, LocoNetInterface throttledController, boolean mTurnoutNoRetry) { 051 super(memo); 052 this.fastcontroller = memo.getLnTrafficController(); 053 this.throttledcontroller = throttledController; 054 this.mTurnoutNoRetry = mTurnoutNoRetry; 055 056 if (fastcontroller != null) { 057 fastcontroller.addLocoNetListener(~0, this); 058 } else { 059 log.error("No layout connection, turnout manager can't function"); 060 } 061 } 062 063 LocoNetInterface fastcontroller; 064 LocoNetInterface throttledcontroller; 065 boolean mTurnoutNoRetry; 066 067 /** 068 * {@inheritDoc} 069 */ 070 @Override 071 @Nonnull 072 public LocoNetSystemConnectionMemo getMemo() { 073 return (LocoNetSystemConnectionMemo) memo; 074 } 075 076 @Override 077 public void dispose() { 078 if (fastcontroller != null) { 079 fastcontroller.removeLocoNetListener(~0, this); 080 } 081 super.dispose(); 082 } 083 084 protected boolean _binaryOutput = false; 085 protected boolean _useOffSwReqAsConfirmation = false; 086 087 public void setUhlenbrockMonitoring() { 088 _binaryOutput = true; 089 mTurnoutNoRetry = true; 090 _useOffSwReqAsConfirmation = true; 091 } 092 093 /** 094 * {@inheritDoc} 095 */ 096 @Nonnull 097 @Override 098 protected Turnout createNewTurnout(@Nonnull String systemName, String userName) throws IllegalArgumentException { 099 String prefix = getSystemPrefix(); 100 int addr; 101 try { 102 addr = Integer.parseInt(systemName.substring(prefix.length() + 1)); 103 } catch (NumberFormatException e) { 104 throw new IllegalArgumentException("Can't convert " + // NOI18N 105 systemName.substring(prefix.length() + 1) + 106 " to LocoNet turnout address"); // NOI18N 107 } 108 LnTurnout t = new LnTurnout(prefix, addr, throttledcontroller); 109 t.setUserName(userName); 110 if (_binaryOutput) t.setBinaryOutput(true); 111 if (_useOffSwReqAsConfirmation) { 112 t.setUseOffSwReqAsConfirmation(true); 113 t.setFeedbackMode("MONITORING"); // NOI18N 114 } 115 return t; 116 } 117 118 // holds last seen turnout request for possible resend 119 LocoNetMessage lastSWREQ = null; 120 121 /** 122 * Listen for turnouts, creating them as needed. 123 */ 124 @Override 125 public void message(LocoNetMessage l) { 126 log.debug("LnTurnoutManager message {}", l); 127 String prefix = getSystemPrefix(); 128 // parse message type 129 int addr; 130 switch (l.getOpCode()) { 131 case LnConstants.OPC_SW_REQ: 132 case LnConstants.OPC_SW_ACK: { /* page 9 of LocoNet PE */ 133 134 int sw1 = l.getElement(1); 135 int sw2 = l.getElement(2); 136 addr = address(sw1, sw2); 137 138 // store message in case resend is needed 139 lastSWREQ = new LocoNetMessage(l); 140 141 // LocoNet spec says 0x10 of SW2 must be 1, but we observe 0 142 if (((sw1 & 0xFC) == 0x78) && ((sw2 & 0xCF) == 0x07)) { 143 return; // turnout interrogate msg 144 } 145 log.debug("SW_REQ received with address {}", addr); 146 break; 147 } 148 case LnConstants.OPC_SW_REP: { /* page 9 of LocoNet PE */ 149 150 // clear resend message, indicating not to resend 151 152 lastSWREQ = null; 153 154 // process this request 155 int sw1 = l.getElement(1); 156 int sw2 = l.getElement(2); 157 addr = address(sw1, sw2); 158 log.debug("SW_REP received with address {}", addr); 159 break; 160 } 161 case LnConstants.OPC_LONG_ACK: { 162 // might have to resend, check 2nd byte 163 if (lastSWREQ != null && l.getElement(1) == 0x30 && l.getElement(2) == 0 && !mTurnoutNoRetry) { 164 // received LONG_ACK reject msg, resend? 165 // Skip if this is a status inquiry 166 int sw1 = lastSWREQ.getElement(1); 167 int sw2 = lastSWREQ.getElement(2); 168 addr = address(sw1, sw2); 169 170 if (addr < 1017 || addr > 1020) { // enquiries are above this 171 fastcontroller.sendLocoNetMessage(lastSWREQ); 172 } 173 } 174 175 // clear so can't resend recursively (we'll see 176 // the resent message echo'd back) 177 lastSWREQ = null; 178 return; 179 } 180 default: // here we didn't find an interesting command 181 // clear resend message, indicating not to resend 182 lastSWREQ = null; 183 return; 184 } 185 // reach here for LocoNet switch command; make sure that a Turnout with this name exists 186 String s = prefix + "T" + addr; // NOI18N 187 LnTurnout lnT = (LnTurnout) getBySystemName(s); 188 if (lnT == null) { 189 // no turnout with this address, is there a light? 190 String sx = prefix + "L" + addr; // NOI18N 191 if (jmri.InstanceManager.lightManagerInstance().getBySystemName(sx) == null) { 192 // no light, create a turnout 193 LnTurnout t = (LnTurnout) provideTurnout(s); 194 195 // process the message to put the turnout in the right state 196 t.messageFromManager(l); 197 } 198 } else { 199 lnT.messageFromManager(l); 200 } 201 } 202 203 private int address(int a1, int a2) { 204 // the "+ 1" in the following converts to throttle-visible numbering 205 return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1); 206 } 207 208 @Override 209 public boolean allowMultipleAdditions(@Nonnull String systemName) { 210 return true; 211 } 212 213 /** 214 * {@inheritDoc} 215 */ 216 @Override 217 public NameValidity validSystemNameFormat(@Nonnull String systemName) { 218 return (getBitFromSystemName(systemName) != 0) ? NameValidity.VALID : NameValidity.INVALID; 219 } 220 221 /** 222 * {@inheritDoc} 223 */ 224 @Override 225 @Nonnull 226 public String validateSystemNameFormat(@Nonnull String systemName, @Nonnull Locale locale) { 227 return validateIntegerSystemNameFormat(systemName, 1, 4096, locale); 228 } 229 230 /** 231 * Get the bit address from the system name. 232 * @param systemName a valid LocoNet-based Turnout System Name 233 * @return the turnout number extracted from the system name 234 */ 235 public int getBitFromSystemName(String systemName) { 236 try { 237 validateSystemNameFormat(systemName, Locale.getDefault()); 238 } catch (IllegalArgumentException ex) { 239 return 0; 240 } 241 return Integer.parseInt(systemName.substring(getSystemNamePrefix().length())); 242 } 243 244 /** 245 * {@inheritDoc} 246 */ 247 @Override 248 public String getEntryToolTip() { 249 return Bundle.getMessage("AddOutputEntryToolTip"); 250 } 251 252 public static final String BYPASSBUSHBYBITKEY = "Bypass Bushby Bit"; 253 public static final String SENDONANDOFFKEY = "Send ON/OFF"; 254 255 /** 256 * {@inheritDoc} 257 */ 258 @Override 259 @Nonnull 260 public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() { 261 List<NamedBeanPropertyDescriptor<?>> l = new ArrayList<>(); 262 l.add(new BooleanPropertyDescriptor(BYPASSBUSHBYBITKEY, false) { 263 @Override 264 public String getColumnHeaderText() { 265 return Bundle.getMessage("LnByPassBushbyHeader"); 266 } 267 268 @Override 269 public boolean isEditable(NamedBean bean) { 270 return bean.getClass().getName().contains("LnTurnout"); 271 } 272 }); 273 l.add(new BooleanPropertyDescriptor(SENDONANDOFFKEY, !_binaryOutput) { 274 @Override 275 public String getColumnHeaderText() { 276 return Bundle.getMessage("SendOnOffHeader"); 277 } 278 279 @Override 280 public boolean isEditable(NamedBean bean) { 281 return bean.getClass().getName().contains("LnTurnout"); 282 } 283 }); 284 return l; 285 } 286 287 private final static Logger log = LoggerFactory.getLogger(LnTurnoutManager.class); 288 289}