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     * <p> 
279     * non-static because it references systemMemo from parent class
280     */
281    class StateMachine<T> extends org.openlcb.MessageDecoder {
282        public StateMachine(@Nonnull Connection connection, @Nonnull NodeID node, @Nonnull T start) {
283            this.connection = connection;
284            this.node = node;
285            this.state = start;
286        }
287
288        final Connection connection;
289        final NodeID node;
290        T state;
291        boolean initizalized = false;
292        protected final HashMap<T, String> stateToEventString = new HashMap<>();
293        protected final HashMap<T, EventID> stateToEventID = new HashMap<>();
294        protected final HashMap<EventID, T> eventToState = new HashMap<>(); // for efficiency, but requires no null entries
295
296        public void setState(@Nonnull T newState) {
297            log.debug("sending PCER to {}", getEventStringForState(newState));
298            connection.put(
299                    new ProducerConsumerEventReportMessage(node, getEventIDForState(newState)),
300                    null);
301        }
302
303        private final EventID nullEvent = new EventID(new byte[]{0,0,0,0,0,0,0,0});
304
305        @Nonnull
306        public T getState() { return state; }
307
308        public void setEventForState(@Nonnull T key, @Nonnull String value) {
309            stateToEventString.put(key, value);
310
311            EventID eid = new OlcbAddress(value, systemMemo).toEventID();
312            stateToEventID.put(key, eid);
313
314            // check for whether already there; so, we're done.
315            if (eventToState.get(eid) == null) {
316                // Not there yet, save it
317                eventToState.put(eid, key);
318
319                if (! nullEvent.equals(eid)) { // and if not the null (i.e. not the "don't send") event
320                    // emit Producer, Consumer Identified messages to show our interest
321                    connection.put(
322                            new ProducerIdentifiedMessage(node, eid, EventState.Unknown),
323                            null);
324                    connection.put(
325                            new ConsumerIdentifiedMessage(node, eid, EventState.Unknown),
326                            null);
327
328                    // emit Identify Producer, Consumer messages to get distributed state
329                    connection.put(
330                            new IdentifyProducersMessage(node, eid),
331                            null);
332                    connection.put(
333                            new IdentifyConsumersMessage(node, eid),
334                            null);
335                }
336            }
337        }
338
339        @CheckForNull
340        public EventID getEventIDForState(@Nonnull T key) {
341            EventID retval = stateToEventID.get(key);
342            if (retval == null) retval = new EventID("00.00.00.00.00.00.00.00");
343            return retval;
344        }
345        @CheckForNull
346        public String getEventStringForState(@Nonnull T key) {
347            String retval = stateToEventString.get(key);
348            if (retval == null) retval = "00.00.00.00.00.00.00.00";
349            return retval;
350        }
351
352        /**
353         * Internal method to determine the EventState for a reply
354         * to an Identify* method
355         * @param event Method returns the underlying state for this EventID
356         * @return State corresponding to the given EventID
357         */
358        EventState getEventIDState(EventID event) {
359            T value = eventToState.get(event);
360            if (initizalized) {
361                if (value.equals(state)) {
362                    return EventState.Valid;
363                } else {
364                    return EventState.Invalid;
365                }
366            } else {
367                return EventState.Unknown;
368            }
369        }
370
371        /**
372         * {@inheritDoc}
373         */
374        @Override
375        public void handleProducerConsumerEventReport(@Nonnull ProducerConsumerEventReportMessage msg, Connection sender){
376            if (eventToState.containsKey(msg.getEventID())) {
377                initizalized = true;
378                state = eventToState.get(msg.getEventID());
379            }
380        }
381        /**
382         * {@inheritDoc}
383         */
384        @Override
385        public void handleProducerIdentified(@Nonnull ProducerIdentifiedMessage msg, Connection sender){
386            // process if for here and marked "valid"
387            if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) {
388                initizalized = true;
389                state = eventToState.get(msg.getEventID());
390            }
391        }
392        /**
393         * {@inheritDoc}
394         */
395        @Override
396        public void handleConsumerIdentified(@Nonnull ConsumerIdentifiedMessage msg, Connection sender){
397            // process if for here and marked "valid"
398            if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) {
399                initizalized = true;
400                state = eventToState.get(msg.getEventID());
401            }
402        }
403
404        /**
405         * {@inheritDoc}
406         */
407        @Override
408        public void handleIdentifyEventsAddressed(@Nonnull IdentifyEventsAddressedMessage msg,
409                                                  Connection sender){
410            // ours?
411            if (! node.equals(msg.getDestNodeID())) return;  // not to us
412            sendAllIdentifiedMessages();
413        }
414
415        /**
416         * {@inheritDoc}
417         */
418        @Override
419        public void handleIdentifyEventsGlobal(@Nonnull IdentifyEventsGlobalMessage msg,
420                                               Connection sender){
421            sendAllIdentifiedMessages();
422        }
423
424        /**
425         * Used at start up to emit the required messages, and in response to a IdentifyEvents message
426         */
427        public void sendAllIdentifiedMessages() {
428            // identify as consumer and producer in same pass
429            Set<Map.Entry<EventID,T>> set = eventToState.entrySet();
430            for (Map.Entry<EventID,T> entry : set) {
431                EventID event = entry.getKey();
432                connection.put(
433                    new ConsumerIdentifiedMessage(node, event, getEventIDState(event)),
434                    null);
435                connection.put(
436                    new ProducerIdentifiedMessage(node, event, getEventIDState(event)),
437                    null);
438            }
439        }
440        /**
441         * {@inheritDoc}
442         */
443        @Override
444        public void handleIdentifyProducers(@Nonnull IdentifyProducersMessage msg, Connection sender){
445            // process if we have the event
446            EventID event = msg.getEventID();
447            if (eventToState.containsKey(event)) {
448                connection.put(
449                    new ProducerIdentifiedMessage(node, event, getEventIDState(event)),
450                    null);
451            }
452        }
453        /**
454         * {@inheritDoc}
455         */
456        @Override
457        public void handleIdentifyConsumers(@Nonnull IdentifyConsumersMessage msg, Connection sender){
458            // process if we have the event
459            EventID event = msg.getEventID();
460            if (eventToState.containsKey(event)) {
461                connection.put(
462                    new ConsumerIdentifiedMessage(node, event, getEventIDState(event)),
463                    null);
464            }
465        }
466
467    }
468
469    private static final Logger log = LoggerFactory.getLogger(OlcbSignalMast.class);
470
471}
472
473