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