001package jmri.jmrix.marklin; 002 003import java.util.regex.Matcher; 004import java.util.regex.Pattern; 005import jmri.Turnout; 006import jmri.implementation.AbstractTurnout; 007 008/** 009 * Implement a Turnout via Marklin communications. 010 * <p> 011 * This object doesn't listen to the Marklin communications for protocol 012 * auto-detection; protocol is determined at construction from the system name 013 * prefix. More than one Turnout object pointing to a single device is not allowed. 014 * <p> 015 * The system name encodes protocol and address: 016 * <ul> 017 * <li>{@code MTDCC45} — DCC accessory address 45, wire = {@code DCCACCSTART + 44}</li> 018 * <li>{@code MTMM123} — MM/MM2 accessory address 123, wire = {@code MM1ACCSTART + 122}</li> 019 * <li>{@code MTCAN200} — raw CAN wire address 200, used verbatim</li> 020 * <li>{@code MT45} — legacy unprefixed, treated as DCC</li> 021 * </ul> 022 * <p> 023 * Based on work by Bob Jacobsen 024 * 025 * @author Kevin Dickerson Copyright (C) 2012 026 * @author Ralf Lang 027 */ 028public class MarklinTurnout extends AbstractTurnout implements MarklinListener { 029 030 /** Matches the address portion after stripping system-prefix + 'T'. */ 031 private static final Pattern ADDRESS_PATTERN = 032 Pattern.compile("^(MM|DCC|CAN)?(\\d+)$"); 033 034 enum Protocol { MM, DCC, CAN } 035 036 private final MarklinTrafficController tc; 037 private final int _number; 038 private final Protocol protocol; 039 040 /** 041 * Create a MarklinTurnout from its full system name. 042 * 043 * @param systemName full system name, e.g. {@code MTDCC45} or {@code MT123} 044 * @param prefix system prefix, e.g. {@code M} 045 * @param tc traffic controller 046 */ 047 public MarklinTurnout(String systemName, String prefix, MarklinTrafficController tc) { 048 super(systemName); 049 this.tc = tc; 050 051 // Strip prefix + 'T' to get the address string, e.g. "DCC45" or "123" 052 String addr = systemName.substring(prefix.length() + 1); 053 Matcher m = ADDRESS_PATTERN.matcher(addr); 054 if (!m.matches()) { 055 throw new IllegalArgumentException("Cannot parse Marklin turnout address from '" + systemName + "'"); 056 } 057 String protocolStr = m.group(1); // null if unprefixed 058 _number = Integer.parseInt(m.group(2)); 059 if ("MM".equals(protocolStr)) { 060 protocol = Protocol.MM; 061 } else if ("CAN".equals(protocolStr)) { 062 protocol = Protocol.CAN; 063 } else { 064 protocol = Protocol.DCC; // DCC explicit or unprefixed legacy 065 } 066 067 tc.addMarklinListener(this); 068 } 069 070 /** {@inheritDoc} */ 071 @Override 072 protected void forwardCommandChangeToLayout(int newState) { 073 // implementing classes will typically have a function/listener to get 074 // updates from the layout, which will then call 075 // public void firePropertyChange(String propertyName, 076 // Object oldValue, 077 // Object newValue) 078 // _once_ if anything has changed state (or set the commanded state directly) 079 080 // sort out states 081 if ((newState & Turnout.CLOSED) != 0) { 082 // first look for the double case, which we can't handle 083 if ((newState & Turnout.THROWN) != 0) { 084 // this is the disaster case! 085 log.error("Cannot command both CLOSED and THROWN {}", newState); 086 } else { 087 // send a CLOSED command 088 sendMessage(!getInverted()); 089 } 090 } else { 091 // send a THROWN command 092 sendMessage(getInverted()); 093 } 094 } 095 096 /** 097 * Return the wire address for the accessory command. 098 * <ul> 099 * <li>DCC: {@code _number + DCCACCSTART - 1}</li> 100 * <li>MM: {@code _number + MM1ACCSTART - 1}</li> 101 * <li>CAN: {@code _number} verbatim (raw CAN address)</li> 102 * </ul> 103 */ 104 int getCANAddress() { 105 switch (protocol) { 106 case MM: return _number + MarklinConstants.MM1ACCSTART - 1; 107 case CAN: return _number; 108 default: return _number + MarklinConstants.DCCACCSTART - 1; // DCC 109 } 110 } 111 112 protected void sendMessage(final boolean newstate) { 113 MarklinMessage msg = MarklinMessage.getSetTurnout(getCANAddress(), (newstate ? 1 : 0), 0x01); 114 tc.sendMarklinMessage(msg, this); 115 jmri.util.TimerUtil.schedule(new java.util.TimerTask() { 116 private final boolean state = newstate; 117 @Override 118 public void run() { 119 try { 120 sendOffMessage(state ? 1 : 0); 121 } catch (Exception e) { 122 log.error("Exception sending delayed off to turnout", e); 123 } 124 } 125 }, METERINTERVAL); 126 } 127 128 protected void sendOffMessage(int state) { 129 MarklinMessage msg = MarklinMessage.getSetTurnout(getCANAddress(), state, 0x00); 130 tc.sendMarklinMessage(msg, this); 131 } 132 133 @Override 134 public void reply(MarklinReply m) { 135 if (m.getPriority() == MarklinConstants.PRIO_1 136 && m.getCommand() >= MarklinConstants.ACCCOMMANDSTART 137 && m.getCommand() <= MarklinConstants.ACCCOMMANDEND 138 && m.getAddress() == getCANAddress()) { 139 switch (m.getElement(9)) { 140 case 0x00: setKnownStateFromCS(Turnout.THROWN); break; 141 case 0x01: setKnownStateFromCS(Turnout.CLOSED); break; 142 default: log.warn("Unknown state command {}", m.getElement(9)); 143 } 144 } 145 } 146 147 @Override 148 public void message(MarklinMessage m) { 149 } 150 151 @Override 152 public void turnoutPushbuttonLockout(boolean b) { 153 } 154 155 /** 156 * Marklin turnouts can be inverted. 157 */ 158 @Override 159 public boolean canInvert() { 160 return true; 161 } 162 163 /** 164 * Set the turnout known state to reflect what's been observed from the 165 * command station messages. A change there means that somebody commanded a 166 * state change (by using a throttle), and that command has 167 * already taken effect. Hence we use "newCommandedState" to indicate it's 168 * taken place. Must be followed by "newKnownState" to complete the turnout 169 * action. 170 * 171 * @param state Observed state, updated state from command station 172 */ 173 synchronized void setCommandedStateFromCS(int state) { 174 if (getFeedbackMode() != DIRECT) return; 175 newCommandedState(state); 176 } 177 178 /** 179 * Set the turnout known state to reflect what's been observed from the 180 * command station messages. A change there means that somebody commanded a 181 * state change (by using a throttle), and that command has 182 * already taken effect. Hence we use "newKnownState" to indicate it's taken 183 * place. 184 * 185 * @param state Observed state, updated state from command station 186 */ 187 synchronized void setKnownStateFromCS(int state) { 188 newCommandedState(state); 189 if (getFeedbackMode() == DIRECT) newKnownState(state); 190 } 191 192 @Override 193 public void dispose() { 194 tc.removeMarklinListener(this); 195 super.dispose(); 196 } 197 198 static final int METERINTERVAL = 100; // ms between on and off pulse 199 200 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MarklinTurnout.class); 201}