001package jmri.jmrix.mqtt; 002 003import javax.annotation.Nonnull; 004import jmri.Turnout; 005import jmri.implementation.AbstractTurnout; 006 007/** 008 * Implementation of the Turnout interface for MQTT layouts. 009 * 010 * @author Lionel Jeanson Copyright (c) 2017 011 * @author Bob Jacobsen Copyright (c) 2020 012 */ 013public class MqttTurnout extends AbstractTurnout implements MqttEventListener { 014 015 private final MqttAdapter mqttAdapter; 016 private final String sendTopic; 017 private final String rcvTopic; 018 019 /** 020 * Requires, but does not check, that the system name and topic be consistent 021 * @param ma Adapter to reference for connection 022 * @param systemName System name of turnout 023 * @param sendTopic MQTT topic to use when sending (full string, including systemName part) 024 * @param rcvTopic MQTT topic to use when receiving (full string, including systemName part) 025 */ 026 MqttTurnout(MqttAdapter ma, String systemName, String sendTopic, String rcvTopic) { 027 super(systemName); 028 this.sendTopic = sendTopic; 029 this.rcvTopic = rcvTopic; 030 mqttAdapter = ma; 031 mqttAdapter.subscribe(rcvTopic, this); // only receive receive topic, not send one 032 _validFeedbackNames = new String[] {"DIRECT", "ONESENSOR", "TWOSENSOR", "DELAYED", "MONITORING"}; 033 _validFeedbackModes = new int[] {DIRECT, ONESENSOR, TWOSENSOR, DELAYED, MONITORING}; 034 _validFeedbackTypes = DIRECT | ONESENSOR | TWOSENSOR | DELAYED | MONITORING; 035 } 036 037 public void setParser(MqttContentParser<Turnout> parser) { 038 this.parser = parser; 039 } 040 041 MqttContentParser<Turnout> parser = new MqttContentParser<Turnout>() { 042 // public for scripting 043 public final static String closedText = "CLOSED"; 044 public final static String thrownText = "THROWN"; 045 public final static String unknownText = "UNKNOWN"; 046 public final static String inconsistentText = "INCONSISTENT"; 047 048 int stateFromString(String payload) { 049 switch (payload) { 050 case closedText: 051 return CLOSED; 052 case thrownText: 053 return THROWN; 054 case unknownText: 055 return UNKNOWN; 056 case inconsistentText: 057 return INCONSISTENT; 058 default: 059 log.warn("{} saw unknown state : {}", getDisplayName(), payload); 060 return UNKNOWN; 061 } 062 } 063 064 @Override 065 public void beanFromPayload(@Nonnull Turnout bean, @Nonnull String payload, @Nonnull String topic) { 066 int state = stateFromString(payload); 067 068 boolean couldBeSendMessage = topic.endsWith(sendTopic); // not listening for send messages, but can get them anyway 069 boolean couldBeRcvMessage = topic.endsWith(rcvTopic); 070 071 if (couldBeSendMessage) { 072 // always accept as commadn 073 newCommandedState(state); 074 075 // when needed, do feedback 076 if (getFeedbackMode() == DIRECT || getFeedbackMode() == MONITORING) newKnownState(state); 077 078 return; 079 } 080 081 if (couldBeRcvMessage) { 082 083 // if MONITORING, do feedback 084 if (getFeedbackMode() == DIRECT || getFeedbackMode() == MONITORING) newKnownState(state); 085 086 return; 087 } 088 089 // really shouldn't have gotten here 090 log.warn("expected failure to decode topic {} {}", topic, payload); 091 return; 092 } 093 094 @Override 095 public @Nonnull String payloadFromBean(@Nonnull Turnout bean, int newState) { 096 // calls jmri.implementation.AbstractTurnout#stateChangeCheck(int) 097 String text = ""; 098 try { 099 text = (stateChangeCheck(newState) ? closedText : thrownText); 100 } catch (IllegalArgumentException ex) { 101 log.error("new state invalid, Turnout not set"); 102 } 103 return text; 104 } 105 }; 106 107 // MQTT Turnouts do support inversion 108 @Override 109 public boolean canInvert() { 110 return true; 111 } 112 113 /** 114 * {@inheritDoc} 115 * Sends an MQTT payload command 116 */ 117 @Override 118 protected void forwardCommandChangeToLayout(int s) { 119 // sort out states 120 String payload = parser.payloadFromBean(this, s); 121 122 // send appropriate command 123 sendMessage(payload); 124 } 125 126 private void sendMessage(String c) { 127 mqttAdapter.publish(sendTopic, c); 128 } 129 130 @Override 131 public void notifyMqttMessage(String receivedTopic, String message) { 132 if (! ( receivedTopic.endsWith(rcvTopic) || receivedTopic.endsWith(sendTopic) ) ) { 133 log.error("{} got a message whose topic ({}) wasn't for me ({})", getDisplayName(), receivedTopic, rcvTopic); 134 return; 135 } 136 137 parser.beanFromPayload(this, message, receivedTopic); 138 } 139 140 @Override 141 protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) { 142 log.warn("Send command to {} Pushbutton in {} not yet coded", (_pushButtonLockout ? "Lock" : "Unlock"), getSystemName()); 143 } 144 145 @Override 146 public void dispose() { 147 mqttAdapter.unsubscribe(rcvTopic, this); 148 super.dispose(); 149 } 150 151 152 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MqttTurnout.class); 153 154}