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        
085    // not sure why this is a CanSystemConnectionMemo in simulator, but it is 
086    jmri.jmrix.can.CanSystemConnectionMemo systemMemo;
087
088    protected void configureFromName(String systemName) {
089        // split out the basic information
090        String[] parts = systemName.split(":");
091        if (parts.length < 3) {
092            log.error("SignalMast system name needs at least three parts: {}",systemName);
093            throw new IllegalArgumentException("System name needs at least three parts: " + systemName);
094        }
095        if (!parts[0].endsWith(mastType)) {
096            log.warn("First part of SignalMast system name is incorrect {} : {}",systemName,mastType);
097        } else {
098            String systemPrefix = parts[0].substring(0, parts[0].indexOf('$') - 1);
099            java.util.List<SystemConnectionMemo> memoList = jmri.InstanceManager.getList(SystemConnectionMemo.class);
100
101            for (SystemConnectionMemo memo : memoList) {
102                if (memo.getSystemPrefix().equals(systemPrefix)) {
103                    if (memo instanceof jmri.jmrix.can.CanSystemConnectionMemo) {
104                        systemMemo = (jmri.jmrix.can.CanSystemConnectionMemo) memo;
105                    } else {
106                        log.error("Can't create mast \"{}\" because system \"{}\" is not CanSystemConnectionMemo but rather {}"
107                                ,systemName,systemPrefix,memo.getClass());
108                    }
109                    break;
110                }
111            }
112
113            if (systemMemo == null) {
114                log.error("No OpenLCB connection found for system prefix \"{}\", so mast \"{}\" will not function",
115                        systemPrefix,systemName);
116            }
117        }
118        String system = parts[1];
119        String mast = parts[2];
120
121        mast = mast.substring(0, mast.indexOf('('));
122        setMastType(mast);
123        String tmp = parts[2].substring(parts[2].indexOf("($") + 2, parts[2].indexOf(')')); // +2 because we're looking for 2 characters
124        
125        try {
126            mastNumber = Integer.parseInt(tmp);
127            if (mastNumber > lastRef) {
128                setLastRef(mastNumber);
129            }
130        } catch (NumberFormatException e) {
131            log.warn("Mast number of SystemName {} is not in the correct format: {} is not an integer", systemName, tmp);
132        }
133        configureSignalSystemDefinition(system);
134        configureAspectTable(system, mast);
135
136        if (systemMemo != null) { // initialization that requires a connection, normally present
137            node = ((OlcbInterface)systemMemo.get(OlcbInterface.class)).getNodeId();
138            connection = ((OlcbInterface)systemMemo.get(OlcbInterface.class)).getOutputConnection();
139 
140            litMachine = new StateMachine<>(connection, node, Boolean.TRUE);
141            heldMachine = new StateMachine<>(connection, node, Boolean.FALSE);
142            aspectMachine = new StateMachine<>(connection, node, getAspect());
143        
144            ((OlcbInterface)systemMemo.get(OlcbInterface.class)).registerMessageListener(new MessageDecoder(){
145                @Override
146                public void put(Message msg, Connection sender) {
147                    handleMessage(msg);
148                }
149            });
150
151        }   
152    }
153
154    int mastNumber; // used to tell them apart
155    
156    public void setOutputForAppearance(String appearance, String event) {
157        aspectMachine.setEventForState(appearance, event);
158    }
159
160    public boolean isOutputConfigured(String appearance) {
161        return aspectMachine.getEventStringForState(appearance) != null;
162    }
163    
164    public String getOutputForAppearance(String appearance) {
165        String retval = aspectMachine.getEventStringForState(appearance);
166        if (retval == null) {
167            log.error("Trying to get appearance {} but it has not been configured",appearance);
168            return "";
169        }
170        return retval;
171    }
172
173    @Override
174    public void setAspect(@Nonnull String aspect) {
175        aspectMachine.setState(aspect);
176        // Normally, the local state is changed by super.setAspect(aspect); here; see comment at top
177    }
178
179    /**
180     * Handle incoming messages.
181     * 
182     * @param msg the message to handle.
183     */
184    public void handleMessage(Message msg) {
185        // gather before state
186        Boolean litBefore = litMachine.getState();
187        Boolean heldBefore = heldMachine.getState();
188        String aspectBefore = aspectMachine.getState(); // before the update
189        
190        // handle message
191        msg.applyTo(litMachine, null);
192        msg.applyTo(heldMachine, null);
193        msg.applyTo(aspectMachine, null);
194        
195        // handle changes, if any
196        if (!litBefore.equals(litMachine.getState())) firePropertyChange("Lit", litBefore, litMachine.getState());
197        if (!heldBefore.equals(heldMachine.getState())) firePropertyChange("Held", heldBefore, heldMachine.getState());
198        
199        this.aspect = aspectMachine.getState();  // after the update
200        this.speed = (String) getSignalSystem().getProperty(aspect, "speed");
201        // need to check aspect != null because original getAspect (at ctor time) can return null, even though StateMachine disallows it.
202        if (aspect==null || ! aspect.equals(aspectBefore)) firePropertyChange("Aspect", aspectBefore, aspect);
203
204    }
205    
206    /** 
207     * Always communicates via OpenLCB
208     */
209    @Override
210    public void setLit(boolean newLit) {
211        litMachine.setState(newLit);
212        // does not call super.setLit because no local state change until Event consumed
213    }
214    @Override
215    public boolean getLit() {
216        return litMachine.getState();
217    }
218
219    /** 
220     * Always communicates via OpenLCB
221     */
222    @Override
223    public void setHeld(boolean newHeld) {
224        heldMachine.setState(newHeld);
225        // does not call super.setHeld because no local state change until Event consumed
226    }
227    @Override
228    public boolean getHeld() {
229        return heldMachine.getState();
230    }
231
232    /**
233     *
234     * @param newVal for ordinal of all OlcbSignalMasts in use
235     */
236    protected static void setLastRef(int newVal) {
237        lastRef = newVal;
238    }
239
240    /**
241     * Provide the last used sequence number of all OlcbSignalMasts in use.
242     * @return last used OlcbSignalMasts sequence number
243     */
244    public static int getLastRef() {
245        return lastRef;
246    }
247    protected static volatile int lastRef = 0;
248    // TODO narrow access variable
249    //private static volatile int lastRef = 0;
250
251    public void setLitEventId(String event) { litMachine.setEventForState(Boolean.TRUE, event); }
252    public String getLitEventId() { return litMachine.getEventStringForState(Boolean.TRUE); }
253    public void setNotLitEventId(String event) { litMachine.setEventForState(Boolean.FALSE, event); }
254    public String getNotLitEventId() { return litMachine.getEventStringForState(Boolean.FALSE); }
255
256    public void setHeldEventId(String event) { heldMachine.setEventForState(Boolean.TRUE, event); }
257    public String getHeldEventId() { return heldMachine.getEventStringForState(Boolean.TRUE); }
258    public void setNotHeldEventId(String event) { heldMachine.setEventForState(Boolean.FALSE, event); }
259    public String getNotHeldEventId() { return heldMachine.getEventStringForState(Boolean.FALSE); }
260
261    /**
262     * Implement a general state machine where state transitions are 
263     * associated with the production and consumption of specific events.
264     * There's a one-to-one mapping between transitions and events.
265     * EventID storage is via Strings, so that the user-visible 
266     * eventID string is preserved.
267     */
268    static class StateMachine<T> extends org.openlcb.MessageDecoder {
269        public StateMachine(@Nonnull Connection connection, @Nonnull NodeID node, @Nonnull T start) {
270            this.connection = connection;
271            this.node = node;
272            this.state = start;
273        }
274        
275        final Connection connection;
276        final NodeID node;
277        T state;
278        boolean initizalized = false;
279        protected final HashMap<T, String> stateToEventString = new HashMap<>();
280        protected final HashMap<T, EventID> stateToEventID = new HashMap<>();
281        protected final HashMap<EventID, T> eventToState = new HashMap<>(); // for efficiency, but requires no null entries
282        
283        public void setState(@Nonnull T newState) {
284            log.debug("sending PCER to {}", getEventStringForState(newState));
285            connection.put(
286                    new ProducerConsumerEventReportMessage(node, getEventIDForState(newState)),
287                    null);
288        }
289        
290        private static final EventID nullEvent = new EventID(new byte[]{0,0,0,0,0,0,0,0});
291        
292        @Nonnull
293        public T getState() { return state; }
294        
295        public void setEventForState(@Nonnull T key, @Nonnull String value) {
296            stateToEventString.put(key, value);
297
298            EventID eid = new OlcbAddress(value).toEventID();
299            stateToEventID.put(key, eid);
300            
301            // check for whether already there; so, we're done.
302            if (eventToState.get(eid) == null) {
303                // Not there yet, save it
304                eventToState.put(eid, key);
305            
306                if (! nullEvent.equals(eid)) { // and if not the null (i.e. not the "don't send") event
307                    // emit Producer, Consumer Identified messages to show our interest
308                    connection.put(
309                            new ProducerIdentifiedMessage(node, eid, EventState.Unknown),
310                            null);
311                    connection.put(
312                            new ConsumerIdentifiedMessage(node, eid, EventState.Unknown),
313                            null);
314
315                    // emit Identify Producer, Consumer messages to get distributed state
316                    connection.put(
317                            new IdentifyProducersMessage(node, eid),
318                            null);
319                    connection.put(
320                            new IdentifyConsumersMessage(node, eid),
321                            null);
322                }
323            }
324        }
325        
326        @CheckForNull
327        public EventID getEventIDForState(@Nonnull T key) {
328            EventID retval = stateToEventID.get(key);
329            if (retval == null) retval = new EventID("00.00.00.00.00.00.00.00");
330            return retval;
331        }
332        @CheckForNull
333        public String getEventStringForState(@Nonnull T key) {
334            String retval = stateToEventString.get(key);
335            if (retval == null) retval = "00.00.00.00.00.00.00.00";
336            return retval;
337        }
338
339        /**
340         * Internal method to determine the EventState for a reply
341         * to an Identify* method
342         * @param event Method returns the underlying state for this EventID
343         * @return State corresponding to the given EventID
344         */
345        EventState getEventIDState(EventID event) {
346            T value = eventToState.get(event);
347            if (initizalized) {
348                if (value.equals(state)) {
349                    return EventState.Valid;
350                } else {
351                    return EventState.Invalid;
352                }
353            } else {
354                return EventState.Unknown;
355            }
356        }
357
358        /**
359         * {@inheritDoc}
360         */
361        @Override
362        public void handleProducerConsumerEventReport(@Nonnull ProducerConsumerEventReportMessage msg, Connection sender){
363            if (eventToState.containsKey(msg.getEventID())) {
364                initizalized = true;
365                state = eventToState.get(msg.getEventID());
366            }
367        }
368        /**
369         * {@inheritDoc}
370         */
371        @Override
372        public void handleProducerIdentified(@Nonnull ProducerIdentifiedMessage msg, Connection sender){
373            // process if for here and marked "valid"
374            if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) {
375                initizalized = true;
376                state = eventToState.get(msg.getEventID());
377            }
378        }
379        /**
380         * {@inheritDoc}
381         */
382        @Override
383        public void handleConsumerIdentified(@Nonnull ConsumerIdentifiedMessage 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        /**
392         * {@inheritDoc}
393         */
394        @Override
395        public void handleIdentifyEventsAddressed(@Nonnull IdentifyEventsAddressedMessage msg,
396                                                  Connection sender){
397            // ours?
398            if (! node.equals(msg.getDestNodeID())) return;  // not to us
399            sendAllIdentifiedMessages();
400        }
401
402        /**
403         * {@inheritDoc}
404         */
405        @Override
406        public void handleIdentifyEventsGlobal(@Nonnull IdentifyEventsGlobalMessage msg,
407                                               Connection sender){
408            sendAllIdentifiedMessages();
409        }
410
411        /**
412         * Used at start up to emit the required messages, and in response to a IdentifyEvents message
413         */
414        public void sendAllIdentifiedMessages() {
415            // identify as consumer and producer in same pass
416            Set<Map.Entry<EventID,T>> set = eventToState.entrySet();
417            for (Map.Entry<EventID,T> entry : set) {
418                EventID event = entry.getKey();
419                connection.put(
420                    new ConsumerIdentifiedMessage(node, event, getEventIDState(event)),
421                    null);
422                connection.put(
423                    new ProducerIdentifiedMessage(node, event, getEventIDState(event)),
424                    null);
425            }
426        }
427        /**
428         * {@inheritDoc}
429         */
430        @Override
431        public void handleIdentifyProducers(@Nonnull IdentifyProducersMessage msg, Connection sender){
432            // process if we have the event
433            EventID event = msg.getEventID();
434            if (eventToState.containsKey(event)) {
435                connection.put(
436                    new ProducerIdentifiedMessage(node, event, getEventIDState(event)),
437                    null);
438            }
439        }
440        /**
441         * {@inheritDoc}
442         */
443        @Override
444        public void handleIdentifyConsumers(@Nonnull IdentifyConsumersMessage 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 ConsumerIdentifiedMessage(node, event, getEventIDState(event)),
450                    null);
451            }
452        }
453        
454    }
455    
456    private static final Logger log = LoggerFactory.getLogger(OlcbSignalMast.class);
457
458}
459
460