001package jmri.jmrix.openlcb;
002
003import java.util.*;
004import javax.annotation.*;
005
006import jmri.implementation.AbstractSignalMast;
007import jmri.SystemConnectionMemo;
008
009import org.openlcb.Connection;
010import org.openlcb.EventID;
011import org.openlcb.EventState;
012import org.openlcb.IdentifyEventsAddressedMessage;
013import org.openlcb.IdentifyEventsGlobalMessage;
014import org.openlcb.Message;
015import org.openlcb.MessageDecoder;
016import org.openlcb.NodeID;
017import org.openlcb.OlcbInterface;
018import org.openlcb.ProducerConsumerEventReportMessage;
019import org.openlcb.IdentifyConsumersMessage;
020import org.openlcb.ConsumerIdentifiedMessage;
021import org.openlcb.IdentifyProducersMessage;
022import org.openlcb.ProducerIdentifiedMessage;
023
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * This class implements a SignalMast that use <B>OpenLCB Events</B>
029 * to set aspects.
030 * <p>
031 * This implementation writes out to the OpenLCB when it's commanded to
032 * change appearance, and updates its internal state when it hears Events from
033 * the network (including its own events).
034 * <p>
035 * System name specifies the creation information:
036 * <pre>
037 * IF$dsm:basic:one-searchlight(123)
038 * </pre> The name is a colon-separated series of terms:
039 * <ul>
040 * <li>I - system prefix
041 * <li>F$olm - defines signal masts of this type
042 * <li>basic - name of the signaling system
043 * <li>one-searchlight - name of the particular aspect map
044 * <li>($123) - number distinguishing this from others
045 * </ul>
046 * <p>
047 * EventIDs are returned in format in which they were provided.
048 * <p>
049 * To keep OpenLCB distributed state consistent, {@link #setAspect} does not immediately
050 * change the local aspect.  Instead, it produces the relevant EventId on the
051 * network, waiting for that to return and do the local state change, notification, etc.
052 * <p>
053 * Needs to have held/unheld, lit/unlit state completed - those need to Produce and Consume events as above
054 * Based upon {@link jmri.implementation.DccSignalMast} by Kevin Dickerson
055 *
056 * @author Bob Jacobsen    Copyright (c) 2017, 2018
057 */
058public class OlcbSignalMast extends AbstractSignalMast {
059
060    public OlcbSignalMast(String sys, String user) {
061        super(sys, user);
062        configureFromName(sys);
063    }
064
065    public OlcbSignalMast(String sys) {
066        super(sys);
067        configureFromName(sys);
068    }
069
070    public OlcbSignalMast(String sys, String user, String mastSubType) {
071        super(sys, user);
072        mastType = mastSubType;
073        configureFromName(sys);
074    }
075
076    protected String mastType = "F$olm";
077
078    StateMachine<Boolean> litMachine;
079    StateMachine<Boolean> heldMachine;
080    StateMachine<String> aspectMachine;
081
082    NodeID node;
083    Connection connection;
084    String systemPrefix;
085
086    public String getSystemPrefix() {
087        return systemPrefix;
088    }
089
090    // not sure why this is a CanSystemConnectionMemo in simulator, but it is
091    jmri.jmrix.can.CanSystemConnectionMemo systemMemo;
092
093    protected void configureFromName(String systemName) {
094        // split out the basic information
095        String[] parts = systemName.split(":");
096        if (parts.length < 3) {
097            log.error("SignalMast system name needs at least three parts: {}",systemName);
098            throw new IllegalArgumentException("System name needs at least three parts: " + systemName);
099        }
100        if (!parts[0].endsWith(mastType)) {
101            systemPrefix = null;
102            log.warn("First part of SignalMast system name is incorrect {} : {}",systemName,mastType);
103        } else {
104            systemPrefix = parts[0].substring(0, parts[0].indexOf('$') - 1);
105            java.util.List<SystemConnectionMemo> memoList = jmri.InstanceManager.getList(SystemConnectionMemo.class);
106
107            for (SystemConnectionMemo memo : memoList) {
108                if (memo.getSystemPrefix().equals(systemPrefix)) {
109                    if (memo instanceof jmri.jmrix.can.CanSystemConnectionMemo) {
110                        systemMemo = (jmri.jmrix.can.CanSystemConnectionMemo) memo;
111                    } else {
112                        log.error("Can't create mast \"{}\" because system \"{}\" is not CanSystemConnectionMemo but rather {}"
113                                ,systemName,systemPrefix,memo.getClass());
114                    }
115                    break;
116                }
117            }
118
119            if (systemMemo == null) {
120                log.error("No OpenLCB connection found for system prefix \"{}\", so mast \"{}\" will not function",
121                        systemPrefix,systemName);
122            }
123        }
124        String system = parts[1];
125        String mast = parts[2];
126
127        mast = mast.substring(0, mast.indexOf('('));
128        setMastType(mast);
129        String tmp = parts[2].substring(parts[2].indexOf("($") + 2, parts[2].indexOf(')')); // +2 because we're looking for 2 characters
130
131        try {
132            mastNumber = Integer.parseInt(tmp);
133            if (mastNumber > lastRef) {
134                setLastRef(mastNumber);
135            }
136        } catch (NumberFormatException e) {
137            log.warn("Mast number of SystemName {} is not in the correct format: {} is not an integer", systemName, tmp);
138        }
139        configureSignalSystemDefinition(system);
140        configureAspectTable(system, mast);
141
142        if (systemMemo != null) { // initialization that requires a connection, normally present
143            node = systemMemo.get(OlcbInterface.class).getNodeId();
144            connection = systemMemo.get(OlcbInterface.class).getOutputConnection();
145
146            litMachine = new StateMachine<>(connection, node, Boolean.TRUE);
147            heldMachine = new StateMachine<>(connection, node, Boolean.FALSE);
148            String configureAspect = getAspect();
149            if ( configureAspect == null ) {
150                log.debug("No Starting Aspect set for {}", getDisplayName());
151                configureAspect = "";
152            }
153            aspectMachine = new StateMachine<>(connection, node, configureAspect);
154
155            systemMemo.get(OlcbInterface.class).registerMessageListener(new MessageDecoder(){
156                @Override
157                public void put(Message msg, Connection sender) {
158                    handleMessage(msg);
159                }
160            });
161
162        }
163    }
164
165    int mastNumber; // used to tell them apart
166
167    public void setOutputForAppearance(String appearance, String event) {
168        aspectMachine.setEventForState(appearance, event);
169    }
170
171    public boolean isOutputConfigured(String appearance) {
172        return aspectMachine.getEventStringForState(appearance) != null;
173    }
174
175    public String getOutputForAppearance(String appearance) {
176        String retval = aspectMachine.getEventStringForState(appearance);
177        if (retval == null) {
178            log.error("Trying to get appearance {} but it has not been configured",appearance);
179            return "";
180        }
181        return retval;
182    }
183
184    @Override
185    public void setAspect(@Nonnull String aspect) {
186        aspectMachine.setState(aspect);
187        // Normally, the local state is changed by super.setAspect(aspect); here; see comment at top
188    }
189
190    /**
191     * Handle incoming messages.
192     *
193     * @param msg the message to handle.
194     */
195    public void handleMessage(Message msg) {
196        // gather before state
197        Boolean litBefore = litMachine.getState();
198        Boolean heldBefore = heldMachine.getState();
199        String aspectBefore = aspectMachine.getState(); // before the update
200
201        // handle message
202        msg.applyTo(litMachine, null);
203        msg.applyTo(heldMachine, null);
204        msg.applyTo(aspectMachine, null);
205
206        // handle changes, if any
207        if (!litBefore.equals(litMachine.getState())) firePropertyChange("Lit", litBefore, litMachine.getState());
208        if (!heldBefore.equals(heldMachine.getState())) firePropertyChange("Held", heldBefore, heldMachine.getState());
209
210        this.aspect = aspectMachine.getState();  // after the update
211        this.speed = (String) getSignalSystem().getProperty(aspect, "speed");
212        // need to check aspect != null because original getAspect (at ctor time) can return null, even though StateMachine disallows it.
213        if (aspect==null || ! aspect.equals(aspectBefore)) firePropertyChange("Aspect", aspectBefore, aspect);
214
215    }
216
217    /**
218     * Always communicates via OpenLCB
219     */
220    @Override
221    public void setLit(boolean newLit) {
222        litMachine.setState(newLit);
223        // does not call super.setLit because no local state change until Event consumed
224    }
225    @Override
226    public boolean getLit() {
227        return litMachine.getState();
228    }
229
230    /**
231     * Always communicates via OpenLCB
232     */
233    @Override
234    public void setHeld(boolean newHeld) {
235        heldMachine.setState(newHeld);
236        // does not call super.setHeld because no local state change until Event consumed
237    }
238    @Override
239    public boolean getHeld() {
240        return heldMachine.getState();
241    }
242
243    /**
244     *
245     * @param newVal for ordinal of all OlcbSignalMasts in use
246     */
247    protected static void setLastRef(int newVal) {
248        lastRef = newVal;
249    }
250
251    /**
252     * Provide the last used sequence number of all OlcbSignalMasts in use.
253     * @return last used OlcbSignalMasts sequence number
254     */
255    public static int getLastRef() {
256        return lastRef;
257    }
258    protected static volatile int lastRef = 0;
259    // TODO narrow access variable
260    //private static volatile int lastRef = 0;
261
262    public void setLitEventId(String event) { litMachine.setEventForState(Boolean.TRUE, event); }
263    public String getLitEventId() { return litMachine.getEventStringForState(Boolean.TRUE); }
264    public void setNotLitEventId(String event) { litMachine.setEventForState(Boolean.FALSE, event); }
265    public String getNotLitEventId() { return litMachine.getEventStringForState(Boolean.FALSE); }
266
267    public void setHeldEventId(String event) { heldMachine.setEventForState(Boolean.TRUE, event); }
268    public String getHeldEventId() { return heldMachine.getEventStringForState(Boolean.TRUE); }
269    public void setNotHeldEventId(String event) { heldMachine.setEventForState(Boolean.FALSE, event); }
270    public String getNotHeldEventId() { return heldMachine.getEventStringForState(Boolean.FALSE); }
271
272    /**
273     * Implement a general state machine where state transitions are
274     * associated with the production and consumption of specific events.
275     * There's a one-to-one mapping between transitions and events.
276     * EventID storage is via Strings, so that the user-visible
277     * eventID string is preserved.
278     */
279    static class StateMachine<T> extends org.openlcb.MessageDecoder {
280        public StateMachine(@Nonnull Connection connection, @Nonnull NodeID node, @Nonnull T start) {
281            this.connection = connection;
282            this.node = node;
283            this.state = start;
284        }
285
286        final Connection connection;
287        final NodeID node;
288        T state;
289        boolean initizalized = false;
290        protected final HashMap<T, String> stateToEventString = new HashMap<>();
291        protected final HashMap<T, EventID> stateToEventID = new HashMap<>();
292        protected final HashMap<EventID, T> eventToState = new HashMap<>(); // for efficiency, but requires no null entries
293
294        public void setState(@Nonnull T newState) {
295            log.debug("sending PCER to {}", getEventStringForState(newState));
296            connection.put(
297                    new ProducerConsumerEventReportMessage(node, getEventIDForState(newState)),
298                    null);
299        }
300
301        private static final EventID nullEvent = new EventID(new byte[]{0,0,0,0,0,0,0,0});
302
303        @Nonnull
304        public T getState() { return state; }
305
306        public void setEventForState(@Nonnull T key, @Nonnull String value) {
307            stateToEventString.put(key, value);
308
309            EventID eid = new OlcbAddress(value).toEventID();
310            stateToEventID.put(key, eid);
311
312            // check for whether already there; so, we're done.
313            if (eventToState.get(eid) == null) {
314                // Not there yet, save it
315                eventToState.put(eid, key);
316
317                if (! nullEvent.equals(eid)) { // and if not the null (i.e. not the "don't send") event
318                    // emit Producer, Consumer Identified messages to show our interest
319                    connection.put(
320                            new ProducerIdentifiedMessage(node, eid, EventState.Unknown),
321                            null);
322                    connection.put(
323                            new ConsumerIdentifiedMessage(node, eid, EventState.Unknown),
324                            null);
325
326                    // emit Identify Producer, Consumer messages to get distributed state
327                    connection.put(
328                            new IdentifyProducersMessage(node, eid),
329                            null);
330                    connection.put(
331                            new IdentifyConsumersMessage(node, eid),
332                            null);
333                }
334            }
335        }
336
337        @CheckForNull
338        public EventID getEventIDForState(@Nonnull T key) {
339            EventID retval = stateToEventID.get(key);
340            if (retval == null) retval = new EventID("00.00.00.00.00.00.00.00");
341            return retval;
342        }
343        @CheckForNull
344        public String getEventStringForState(@Nonnull T key) {
345            String retval = stateToEventString.get(key);
346            if (retval == null) retval = "00.00.00.00.00.00.00.00";
347            return retval;
348        }
349
350        /**
351         * Internal method to determine the EventState for a reply
352         * to an Identify* method
353         * @param event Method returns the underlying state for this EventID
354         * @return State corresponding to the given EventID
355         */
356        EventState getEventIDState(EventID event) {
357            T value = eventToState.get(event);
358            if (initizalized) {
359                if (value.equals(state)) {
360                    return EventState.Valid;
361                } else {
362                    return EventState.Invalid;
363                }
364            } else {
365                return EventState.Unknown;
366            }
367        }
368
369        /**
370         * {@inheritDoc}
371         */
372        @Override
373        public void handleProducerConsumerEventReport(@Nonnull ProducerConsumerEventReportMessage msg, Connection sender){
374            if (eventToState.containsKey(msg.getEventID())) {
375                initizalized = true;
376                state = eventToState.get(msg.getEventID());
377            }
378        }
379        /**
380         * {@inheritDoc}
381         */
382        @Override
383        public void handleProducerIdentified(@Nonnull ProducerIdentifiedMessage msg, Connection sender){
384            // process if for here and marked "valid"
385            if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) {
386                initizalized = true;
387                state = eventToState.get(msg.getEventID());
388            }
389        }
390        /**
391         * {@inheritDoc}
392         */
393        @Override
394        public void handleConsumerIdentified(@Nonnull ConsumerIdentifiedMessage msg, Connection sender){
395            // process if for here and marked "valid"
396            if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) {
397                initizalized = true;
398                state = eventToState.get(msg.getEventID());
399            }
400        }
401
402        /**
403         * {@inheritDoc}
404         */
405        @Override
406        public void handleIdentifyEventsAddressed(@Nonnull IdentifyEventsAddressedMessage msg,
407                                                  Connection sender){
408            // ours?
409            if (! node.equals(msg.getDestNodeID())) return;  // not to us
410            sendAllIdentifiedMessages();
411        }
412
413        /**
414         * {@inheritDoc}
415         */
416        @Override
417        public void handleIdentifyEventsGlobal(@Nonnull IdentifyEventsGlobalMessage msg,
418                                               Connection sender){
419            sendAllIdentifiedMessages();
420        }
421
422        /**
423         * Used at start up to emit the required messages, and in response to a IdentifyEvents message
424         */
425        public void sendAllIdentifiedMessages() {
426            // identify as consumer and producer in same pass
427            Set<Map.Entry<EventID,T>> set = eventToState.entrySet();
428            for (Map.Entry<EventID,T> entry : set) {
429                EventID event = entry.getKey();
430                connection.put(
431                    new ConsumerIdentifiedMessage(node, event, getEventIDState(event)),
432                    null);
433                connection.put(
434                    new ProducerIdentifiedMessage(node, event, getEventIDState(event)),
435                    null);
436            }
437        }
438        /**
439         * {@inheritDoc}
440         */
441        @Override
442        public void handleIdentifyProducers(@Nonnull IdentifyProducersMessage msg, Connection sender){
443            // process if we have the event
444            EventID event = msg.getEventID();
445            if (eventToState.containsKey(event)) {
446                connection.put(
447                    new ProducerIdentifiedMessage(node, event, getEventIDState(event)),
448                    null);
449            }
450        }
451        /**
452         * {@inheritDoc}
453         */
454        @Override
455        public void handleIdentifyConsumers(@Nonnull IdentifyConsumersMessage msg, Connection sender){
456            // process if we have the event
457            EventID event = msg.getEventID();
458            if (eventToState.containsKey(event)) {
459                connection.put(
460                    new ConsumerIdentifiedMessage(node, event, getEventIDState(event)),
461                    null);
462            }
463        }
464
465    }
466
467    private static final Logger log = LoggerFactory.getLogger(OlcbSignalMast.class);
468
469}
470
471