001package jmri.jmrix.mqtt;
002
003import javax.annotation.Nonnull;
004import jmri.implementation.*;
005
006/**
007 * SignalMast implemented via MQTT messages
008 * <p>
009 * System name specifies the creation information:
010 * <pre>
011 * IF$mqm:basic:one-searchlight($0001)
012 * </pre> The name is a colon-separated series of terms:
013 * <ul>
014 * <li>IF$mqm - defines signal masts of this type
015 * <li>basic - name of the signaling system
016 * <li>one-searchlight - name of the particular aspect map
017 * <li>($0001) - small ordinal number for telling various signal masts apart
018 * apart
019 * </ul>
020 *
021 * @author Bob Jacobsen Copyright (C) 2009, 2021
022 */
023public class MqttSignalMast extends AbstractSignalMast implements MqttEventListener {
024
025    public MqttSignalMast(String systemName, String userName) {
026        super(systemName, userName);
027        configureFromName(systemName);
028        sendTopic = makeSendTopic(systemName);
029        mqttAdapter = jmri.InstanceManager.getDefault(MqttSystemConnectionMemo.class).getMqttAdapter();
030        if (mqttAdapter!= null) mqttAdapter.subscribe(sendTopic, this);  // receive back on send topic
031    }
032
033    public MqttSignalMast(String systemName) {
034        super(systemName);
035        configureFromName(systemName);
036        sendTopic = makeSendTopic(systemName);
037        mqttAdapter = jmri.InstanceManager.getDefault(MqttSystemConnectionMemo.class).getMqttAdapter();
038        if (mqttAdapter!= null) mqttAdapter.subscribe(sendTopic, this);  // receive back on send topic
039    }
040
041    @Nonnull
042    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "MS_PKGPROTECT",
043            justification = "Public accessibility for scripts that update the prefixes")
044    public static String sendTopicPrefix = "track/signalmast/"; // default for constructing topic; public for script access, set by config
045
046    public static void setSendTopicPrefix(@Nonnull String prefix) {
047        sendTopicPrefix = prefix;
048        log.debug("sendTopicPrefix set to {}", prefix);
049    }
050
051    protected String makeSendTopic(String systemName) {
052        String[] pieces = systemName.split("\\(");
053        if (pieces.length == 2) {
054            String result = pieces[1].substring(1, pieces[1].length()-1); // starts with ($)
055            return sendTopicPrefix+result;
056        } else {
057            log.warn("not just one '(' in {}", systemName);
058            return sendTopicPrefix+systemName;
059        }
060    }
061
062    private static final String mastType = "IF$mqm";
063
064    private final String sendTopic;
065    private MqttAdapter mqttAdapter;
066
067    private void configureFromName(String systemName) {
068        // split out the basic information
069        String[] parts = systemName.split(":");
070        if (parts.length < 3) {
071            log.error("SignalMast system name needs at least three parts: {}", systemName);
072            throw new IllegalArgumentException("System name needs at least three parts: " + systemName);
073        }
074        if (!parts[0].equals(mastType)) {
075            log.warn("SignalMast system name should start with {} but is {}", mastType, systemName);
076        }
077
078        String system = parts[1];
079
080        String mast = parts[2];
081        // new style
082        mast = mast.substring(0, mast.indexOf("("));
083        setMastType(mast);
084        String tmp = parts[2].substring(parts[2].indexOf("($") + 2, parts[2].indexOf(")"));
085        try {
086            log.debug("Parse {} as integer from {}?", tmp, parts[2]);
087            int autoNumber = Integer.parseInt(tmp);
088            synchronized (MqttSignalMast.class) {
089                if (autoNumber > getLastRef()) {
090                    setLastRef(autoNumber);
091                }
092            }
093        } catch (NumberFormatException e) {
094            log.debug("Auto generated SystemName {} does not have numeric form, skipping autoincrement", systemName);
095        }
096        configureSignalSystemDefinition(system);
097        configureAspectTable(system, mast);
098    }
099
100    @Override
101    public void setAspect(@Nonnull String aspect) {
102        // check it's a choice
103        if (!map.checkAspect(aspect)) {
104            // not a valid aspect
105            log.warn("attempting to set invalid aspect: {} on mast: {}", aspect, getDisplayName());
106            throw new IllegalArgumentException("attempting to set invalid aspect: " + aspect + " on mast: " + getDisplayName());
107        } else if (disabledAspects.contains(aspect)) {
108            log.warn("attempting to set an aspect that has been disabled: {} on mast: {}", aspect, getDisplayName());
109            throw new IllegalArgumentException("attempting to set an aspect that has been disabled: " + aspect + " on mast: " + getDisplayName());
110        }
111        log.debug("Setting aspect {}", aspect);
112        super.setAspect(aspect);
113        report();
114    }
115
116    @Override
117    public void setHeld(boolean held) {
118        log.debug("Setting held {}", held);
119        super.setHeld(held);
120        report();
121    }
122
123    @Override
124    public void setLit(boolean lit) {
125        log.debug("Setting lit {}", lit);
126        super.setLit(lit);
127        report();
128    }
129
130    private void report() {
131        String msg = aspect+"; ";
132        msg = msg+ (getLit()?"Lit; ":"Unlit; ");
133        msg = msg+ (getHeld()?"Held":"Unheld");
134        sendMessage(msg);
135    }
136    private void sendMessage(String c) {
137        log.debug("publishing \"{}\" on \"{}\"", c, sendTopic);
138        mqttAdapter.publish(sendTopic, c);
139    }
140
141    /**
142     *
143     * @param newVal for ordinal of all MqttSignalMasts in use
144     */
145    protected static void setLastRef(int newVal) {
146        lastRef = newVal;
147    }
148
149    /**
150     * @return highest ordinal of all MqttSignalMasts in use
151     */
152    public static int getLastRef() {
153        return lastRef;
154    }
155
156    @Override
157    public void notifyMqttMessage(String receivedTopic, String payload) {
158        if (! ( receivedTopic.endsWith(sendTopic) ) ) {
159            log.error("{} got a message whose topic ({}) wasn't for me ({})", getDisplayName(), receivedTopic, sendTopic);
160            return;
161        }
162        
163        // parse and  act
164        var parts =  payload.split(";");
165        int length = parts.length;
166        // part 0 is aspect
167        if (length >= 1) {
168            // parts[0] is the aspect name
169            String aspect = parts[0].trim();
170            // check it's a choice
171            if (!map.checkAspect(aspect)) {
172                // not a valid aspect
173                log.warn("received invalid aspect: {} on mast: {}", aspect, getDisplayName());
174                throw new IllegalArgumentException("attempting to set invalid aspect: " + aspect + " on mast: " + getDisplayName());
175            } else if (disabledAspects.contains(aspect)) {
176                log.warn("received an aspect that has been disabled: {} on mast: {}", aspect, getDisplayName());
177                throw new IllegalArgumentException("attempting to set an aspect that has been disabled: " + aspect + " on mast: " + getDisplayName());
178            }
179            log.debug("Setting aspect {} from received payload", aspect);
180            super.setAspect(aspect);
181            
182        } else {
183            log.error("{} got message with empty payload", getDisplayName());
184        }
185        if (length >= 2) {
186            // parts[1] is the Lit status
187            if (parts[1].trim().equals("Unlit")) {
188                super.setLit(false); // calling this class's setLit sends report
189            } else  {
190                super.setLit(true); // calling this class's setLit sends report
191            }
192        } 
193        if (length >= 3) {
194            // parts[2] is the Held status
195            if (parts[2].trim().equals("Held")) {
196                super.setHeld(true); // calling this class's setHeld sends report
197            } else  {
198                super.setHeld(false); // calling this class's setHeld sends report
199            }
200        }
201    }
202
203    /**
204     * Ordinal of all MqttSignalMasts to create unique system name.
205     */
206    private static volatile int lastRef = 0;
207
208    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MqttSignalMast.class);
209
210}