001package jmri.jmrix.openlcb;
002
003import java.net.InetAddress;
004import java.net.NetworkInterface;
005import java.net.SocketException;
006import java.nio.charset.StandardCharsets;
007import java.util.ArrayList;
008import java.util.List;
009import java.util.ResourceBundle;
010
011import jmri.ClockControl;
012import jmri.GlobalProgrammerManager;
013import jmri.InstanceManager;
014import jmri.jmrix.can.CanListener;
015import jmri.jmrix.can.CanMessage;
016import jmri.jmrix.can.CanReply;
017import jmri.jmrix.can.CanSystemConnectionMemo;
018import jmri.jmrix.can.TrafficController;
019import jmri.profile.ProfileManager;
020import jmri.util.ThreadingUtil;
021
022import org.openlcb.*;
023import org.openlcb.can.AliasMap;
024import org.openlcb.can.CanInterface;
025import org.openlcb.can.MessageBuilder;
026import org.openlcb.can.OpenLcbCanFrame;
027import org.openlcb.implementations.DatagramService;
028import org.openlcb.implementations.MemoryConfigurationService;
029import org.openlcb.protocols.TimeProtocol;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033/**
034 * Does configuration for OpenLCB communications implementations.
035 *
036 * @author Bob Jacobsen Copyright (C) 2010
037 */
038public class OlcbConfigurationManager extends jmri.jmrix.can.ConfigurationManager {
039
040    // Constants for the protocol options keys. These option keys are used to save configuration
041    // in the profile.xml and set on a per-connection basis in the connection preferences.
042
043    // Protocol key for node identification
044    public static final String OPT_PROTOCOL_IDENT = "Ident";
045
046    // Option key for Node ID
047    public static final String OPT_IDENT_NODEID = "NodeId";
048    // Option key for User Name, used for the Simple Node Ident Protocol
049    public static final String OPT_IDENT_USERNAME = "UserName";
050    // Option key for User Description, used for the Simple Node Ident Protocol
051    public static final String OPT_IDENT_DESCRIPTION = "UserDescription";
052
053    // Protocol key for fast clock
054    public static final String OPT_PROTOCOL_FASTCLOCK = "FastClock";
055
056    // Option key for fast clock mode
057    public static final String OPT_FASTCLOCK_ENABLE = "EnableMode";
058    // Option value for setting fast clock to disabled.
059    public static final String OPT_FASTCLOCK_ENABLE_OFF = "disabled";
060    // Option value for setting fast clock to clock generator/producer/master.
061    public static final String OPT_FASTCLOCK_ENABLE_GENERATOR = "generator";
062    // Option value for setting fast clock to clock consumer/slave.
063    public static final String OPT_FASTCLOCK_ENABLE_CONSUMER = "consumer";
064
065    // Option key for setting the clock identifier.
066    public static final String OPT_FASTCLOCK_ID = "ClockId";
067    // Option value for using the well-known clock id "default clock"
068    public static final String OPT_FASTCLOCK_ID_DEFAULT = "default";
069    // Option value for using the well-known clock id "default real-time clock"
070    public static final String OPT_FASTCLOCK_ID_DEFAULT_RT = "realtime";
071    // Option value for using the well-known clock id "alternate clock 1"
072    public static final String OPT_FASTCLOCK_ID_ALT_1 = "alt1";
073    // Option value for using the well-known clock id "alternate clock 2"
074    public static final String OPT_FASTCLOCK_ID_ALT_2 = "alt2";
075    // Option value for using a custom clock ID
076    public static final String OPT_FASTCLOCK_ID_CUSTOM = "custom";
077
078    // Option key for setting the clock identifier to a custom value. Must set ClockId==custom in
079    // order to be in effect. The custom clock id is in node ID format.
080    public static final String OPT_FASTCLOCK_CUSTOM_ID = "ClockCustomId";
081
082    public OlcbConfigurationManager(CanSystemConnectionMemo memo) {
083        super(memo);
084
085        InstanceManager.store(cf = new jmri.jmrix.openlcb.swing.OpenLcbComponentFactory(adapterMemo),
086                jmri.jmrix.swing.ComponentFactory.class);
087        InstanceManager.store(this, OlcbConfigurationManager.class);
088    }
089
090    final jmri.jmrix.swing.ComponentFactory cf;
091
092    private void initializeFastClock() {
093        boolean isMaster;
094        String enableOption = adapterMemo.getProtocolOption(OPT_PROTOCOL_FASTCLOCK, OPT_FASTCLOCK_ENABLE);
095        if (OPT_FASTCLOCK_ENABLE_GENERATOR.equals(enableOption)) {
096            isMaster = true;
097        } else if (OPT_FASTCLOCK_ENABLE_CONSUMER.equals(enableOption)) {
098            isMaster = false;
099        } else {
100            // no clock needed.
101            return;
102        }
103
104        NodeID clockId = null;
105        String clockIdSetting = adapterMemo.getProtocolOption(OPT_PROTOCOL_FASTCLOCK, OPT_FASTCLOCK_ID);
106        if (OPT_FASTCLOCK_ID_DEFAULT.equals(clockIdSetting)) {
107            clockId = TimeProtocol.DEFAULT_CLOCK;
108        } else if (OPT_FASTCLOCK_ID_DEFAULT_RT.equals(clockIdSetting)) {
109            clockId = TimeProtocol.DEFAULT_RT_CLOCK;
110        } else if (OPT_FASTCLOCK_ID_ALT_1.equals(clockIdSetting)) {
111            clockId = TimeProtocol.ALT_CLOCK_1;
112        } else if (OPT_FASTCLOCK_ID_ALT_2.equals(clockIdSetting)) {
113            clockId = TimeProtocol.ALT_CLOCK_2;
114        } else if (OPT_FASTCLOCK_ID_CUSTOM.equals(clockIdSetting)) {
115            String customId = adapterMemo.getProtocolOption(OPT_PROTOCOL_FASTCLOCK, OPT_FASTCLOCK_CUSTOM_ID);
116            if (customId == null || customId.isEmpty()) {
117                log.error("OpenLCB clock initialize: User selected custom clock, but did not provide a Custom Clock ID. Using default clock.");
118            } else {
119                try {
120                    clockId = new NodeID(customId);
121                } catch (IllegalArgumentException e) {
122                    log.error("OpenLCB clock initialize: Custom Clock ID '{}' is in illegal format. Use dotted hex notation like 05.01.01.01.DD.EE", customId);
123                }
124            }
125        }
126        if (clockId == null) {
127            clockId = TimeProtocol.DEFAULT_CLOCK;
128        }
129        log.debug("Creating olcb clock with id {} is_master {}", clockId, isMaster);
130        clockControl = new OlcbClockControl(getInterface(), clockId, isMaster);
131        InstanceManager.setDefault(ClockControl.class, clockControl);
132    }
133
134    @Override
135    public void configureManagers() {
136
137        // create our NodeID
138        getOurNodeID();
139
140        // do the connections
141        tc = adapterMemo.getTrafficController();
142
143        olcbCanInterface = createOlcbCanInterface(nodeID, tc);
144
145        // create JMRI objects
146        InstanceManager.setSensorManager(
147                getSensorManager());
148
149        InstanceManager.setTurnoutManager(
150                getTurnoutManager());
151
152        InstanceManager.setThrottleManager(
153                getThrottleManager());
154
155        InstanceManager.setReporterManager(
156                getReporterManager());
157
158        InstanceManager.setLightManager(
159                getLightManager()
160        );
161
162        if (getProgrammerManager().isAddressedModePossible()) {
163            InstanceManager.store(getProgrammerManager(), jmri.AddressedProgrammerManager.class);
164        }
165        if (getProgrammerManager().isGlobalProgrammerAvailable()) {
166            jmri.InstanceManager.store(getProgrammerManager(), GlobalProgrammerManager.class);
167        }
168
169        // start alias acquisition
170        new StartUpHandler().start();
171
172        OlcbInterface iface = getInterface();
173        loaderClient = new LoaderClient(iface.getOutputConnection(),
174                iface.getMemoryConfigurationService(),
175                iface.getDatagramService());
176        iface.registerMessageListener(loaderClient);
177
178        iface.registerMessageListener(new SimpleNodeIdentInfoHandler());
179        iface.registerMessageListener(new PipRequestHandler());
180
181        initializeFastClock();
182
183        aliasMap = new AliasMap();
184        tc.addCanListener(new CanListener() {
185            @Override
186            public void message(CanMessage m) {
187                if (!m.isExtended() || m.isRtr()) {
188                    return;
189                }
190                aliasMap.processFrame(convertFromCan(m));
191            }
192
193            @Override
194            public void reply(CanReply m) {
195                if (!m.isExtended() || m.isRtr()) {
196                    return;
197                }
198                aliasMap.processFrame(convertFromCan(m));
199            }
200        });
201        messageBuilder = new MessageBuilder(aliasMap);
202    }
203
204    CanInterface olcbCanInterface;
205    TrafficController tc;
206    NodeID nodeID;
207    LoaderClient loaderClient;
208    OlcbClockControl clockControl;
209
210    OlcbInterface getInterface() {
211        return olcbCanInterface.getInterface();
212    }
213
214    // internal to OpenLCB library, should not be exposed
215    AliasMap aliasMap;
216    // internal to OpenLCB library, should not be exposed
217    MessageBuilder messageBuilder;
218
219    /**
220     * Check if a type of manager is provided by this manager.
221     *
222     * @param type the class of manager to check
223     * @return true if the type of manager is provided; false otherwise
224     */
225    @Override
226    public boolean provides(Class<?> type) {
227        if (adapterMemo.getDisabled()) {
228            return false;
229        }
230        if (type.equals(jmri.ThrottleManager.class)) {
231            return true;
232        }
233        if (type.equals(jmri.SensorManager.class)) {
234            return true;
235        }
236        if (type.equals(jmri.TurnoutManager.class)) {
237            return true;
238        }
239        if (type.equals(jmri.ReporterManager.class)) {
240            return true;
241        }
242        if (type.equals(jmri.LightManager.class)) {
243            return true;
244        }
245        if (type.equals(jmri.GlobalProgrammerManager.class)) {
246            return true;
247        }
248        if (type.equals(jmri.AddressedProgrammerManager.class)) {
249            return true;
250        }
251        if (type.equals(AliasMap.class)) {
252            return true;
253        }
254        if (type.equals(MessageBuilder.class)) {
255            return true;
256        }
257        if (type.equals(MimicNodeStore.class)) {
258            return true;
259        }
260        if (type.equals(Connection.class)) {
261            return true;
262        }
263        if (type.equals(MemoryConfigurationService.class)) {
264            return true;
265        }
266        if (type.equals(DatagramService.class)) {
267            return true;
268        }
269        if (type.equals(NodeID.class)) {
270            return true;
271        }
272        if (type.equals(OlcbInterface.class)) {
273            return true;
274        }
275        if (type.equals(CanInterface.class)) {
276            return true;
277        }
278        if (type.equals(ClockControl.class)) {
279            return clockControl != null;
280        }
281        return false; // nothing, by default
282    }
283
284    @SuppressWarnings("unchecked")
285    @Override
286    public <T> T get(Class<?> T) {
287        if (adapterMemo.getDisabled()) {
288            return null;
289        }
290        if (T.equals(jmri.ThrottleManager.class)) {
291            return (T) getThrottleManager();
292        }
293        if (T.equals(jmri.SensorManager.class)) {
294            return (T) getSensorManager();
295        }
296        if (T.equals(jmri.TurnoutManager.class)) {
297            return (T) getTurnoutManager();
298        }
299        if (T.equals(jmri.LightManager.class)) {
300            return (T) getLightManager();
301        }
302        if (T.equals(jmri.ReporterManager.class)) {
303            return (T) getReporterManager();
304        }
305        if (T.equals(jmri.GlobalProgrammerManager.class)) {
306            return (T) getProgrammerManager();
307        }
308        if (T.equals(jmri.AddressedProgrammerManager.class)) {
309            return (T) getProgrammerManager();
310        }
311        if (T.equals(AliasMap.class)) {
312            return (T) aliasMap;
313        }
314        if (T.equals(MessageBuilder.class)) {
315            return (T) messageBuilder;
316        }
317        if (T.equals(MimicNodeStore.class)) {
318            return (T) getInterface().getNodeStore();
319        }
320        if (T.equals(Connection.class)) {
321            return (T) getInterface().getOutputConnection();
322        }
323        if (T.equals(MemoryConfigurationService.class)) {
324            return (T) getInterface().getMemoryConfigurationService();
325        }
326        if (T.equals(DatagramService.class)) {
327            return (T) getInterface().getDatagramService();
328        }
329        if (T.equals(LoaderClient.class)) {
330            return (T) loaderClient;
331        }
332        if (T.equals(NodeID.class)) {
333            return (T) nodeID;
334        }
335        if (T.equals(OlcbInterface.class)) {
336            return (T) getInterface();
337        }
338        if (T.equals(CanInterface.class)) {
339            return (T) olcbCanInterface;
340        }
341        if (T.equals(ClockControl.class)) {
342            return (T) clockControl;
343        }
344        return null; // nothing, by default
345    }
346
347    protected OlcbProgrammerManager programmerManager;
348
349    public OlcbProgrammerManager getProgrammerManager() {
350        if (adapterMemo.getDisabled()) {
351            return null;
352        }
353        if (programmerManager == null) {
354            programmerManager = new OlcbProgrammerManager(adapterMemo);
355        }
356        return programmerManager;
357    }
358
359    protected OlcbThrottleManager throttleManager;
360
361    public OlcbThrottleManager getThrottleManager() {
362        if (adapterMemo.getDisabled()) {
363            return null;
364        }
365        if (throttleManager == null) {
366            throttleManager = new OlcbThrottleManager(adapterMemo);
367        }
368        return throttleManager;
369    }
370
371    protected OlcbTurnoutManager turnoutManager;
372
373    public OlcbTurnoutManager getTurnoutManager() {
374        if (adapterMemo.getDisabled()) {
375            return null;
376        }
377        if (turnoutManager == null) {
378            turnoutManager = new OlcbTurnoutManager(adapterMemo);
379        }
380        return turnoutManager;
381    }
382
383    protected OlcbSensorManager sensorManager;
384
385    public OlcbSensorManager getSensorManager() {
386        if (adapterMemo.getDisabled()) {
387            return null;
388        }
389        if (sensorManager == null) {
390            sensorManager = new OlcbSensorManager(adapterMemo);
391        }
392        return sensorManager;
393    }
394
395    protected OlcbReporterManager reporterManager;
396
397    public OlcbReporterManager getReporterManager() {
398        if (adapterMemo.getDisabled()) {
399            return null;
400        }
401        if (reporterManager == null) {
402            reporterManager = new OlcbReporterManager(adapterMemo);
403        }
404        return reporterManager;
405    }
406
407    @Override
408    public void dispose() {
409        if (turnoutManager != null) {
410            InstanceManager.deregister(turnoutManager, jmri.jmrix.openlcb.OlcbTurnoutManager.class);
411        }
412        if (sensorManager != null) {
413            InstanceManager.deregister(sensorManager, jmri.jmrix.openlcb.OlcbSensorManager.class);
414        }
415        if (lightManager != null) {
416            InstanceManager.deregister(lightManager, jmri.jmrix.openlcb.OlcbLightManager.class);
417        }
418        if (cf != null) {
419            InstanceManager.deregister(cf, jmri.jmrix.swing.ComponentFactory.class);
420        }
421        InstanceManager.deregister(this, OlcbConfigurationManager.class);
422
423        if (clockControl != null) {
424            clockControl.dispose();
425            InstanceManager.deregister(clockControl, ClockControl.class);
426        }
427    }
428
429    protected OlcbLightManager lightManager;
430
431    public OlcbLightManager getLightManager() {
432        if (adapterMemo.getDisabled()) {
433            return null;
434        }
435        if (lightManager == null) {
436            lightManager = new OlcbLightManager(adapterMemo);
437        }
438        return lightManager;
439    }
440
441    class SimpleNodeIdentInfoHandler extends MessageDecoder {
442        /**
443         * Helper function to add a string value to the sequence of bytes to send for SNIP
444         * response content.
445         *
446         * @param addString  string to render into byte stream
447         * @param contents   represents the byte stream that will be sent.
448         * @param maxlength  maximum number of characters to include, not counting terminating null
449         */
450        private void  addStringPart(String addString, List<Byte> contents, int maxlength) {
451            if (addString != null && !addString.isEmpty()) {
452                String value = addString.substring(0,Math.min(maxlength, addString.length()));
453                byte[] bb = value.getBytes(StandardCharsets.UTF_8);
454                for (byte b : bb) {
455                    contents.add(b);
456                }
457            }
458            // terminating null byte.
459            contents.add((byte)0);
460        }
461
462        SimpleNodeIdentInfoHandler() {
463            List<Byte> l = new ArrayList<>(256);
464
465            l.add((byte)4); // version byte
466            addStringPart("JMRI", l, 40);  // mfg field; 40 char limit in Standard, not counting final null
467            addStringPart("PanelPro", l, 40);  // model
468            String name = ProfileManager.getDefault().getActiveProfileName();
469            if (name != null) {
470                addStringPart(name, l, 20); // hardware version
471            } else {
472                addStringPart("", l, 20); // hardware version
473            }
474            addStringPart(jmri.Version.name(), l, 20); // software version
475
476            l.add((byte)2); // version byte
477            addStringPart(adapterMemo.getProtocolOption(OPT_PROTOCOL_IDENT, OPT_IDENT_USERNAME), l, 62);
478            addStringPart(adapterMemo.getProtocolOption(OPT_PROTOCOL_IDENT, OPT_IDENT_DESCRIPTION), l, 63);
479
480            content = new byte[l.size()];
481            for (int i = 0; i < l.size(); ++i) {
482                content[i] = l.get(i);
483            }
484        }
485        private final byte[] content;
486
487        @Override
488        public void handleSimpleNodeIdentInfoRequest(SimpleNodeIdentInfoRequestMessage msg,
489                Connection sender) {
490            if (msg.getDestNodeID().equals(nodeID)) {
491                // Sending a SNIP reply to the bus crashes the library up to 0.7.7.
492                if (msg.getSourceNodeID().equals(nodeID) || Version.libVersionAtLeast(0, 7, 8)) {
493                    getInterface().getOutputConnection().put(new SimpleNodeIdentInfoReplyMessage(nodeID, msg.getSourceNodeID(), content), this);
494                }
495            }
496        }
497    }
498
499    class PipRequestHandler extends MessageDecoder {
500
501        @Override
502        public void handleProtocolIdentificationRequest(ProtocolIdentificationRequestMessage msg, Connection sender) {
503            long flags = 0x00041000000000L;  // PC, SNIP protocols
504            // only reply if for us
505            if (msg.getDestNodeID() == nodeID) {
506                getInterface().getOutputConnection().put(new ProtocolIdentificationReplyMessage(nodeID, msg.getSourceNodeID(), flags), this);
507            }
508        }
509
510    }
511
512    @Override
513    protected ResourceBundle getActionModelResourceBundle() {
514        return ResourceBundle.getBundle("jmri.jmrix.openlcb.OlcbActionListBundle");
515    }
516
517    /**
518     * Create a node ID in the JMRI range from one byte of IP address, and 2
519     * bytes of PID. That changes each time, which isn't perhaps what's wanted.
520     */
521    protected void getOurNodeID() {
522        String userOption = adapterMemo.getProtocolOption(OPT_PROTOCOL_IDENT, OPT_IDENT_NODEID);
523        if (userOption != null && !userOption.isEmpty()) {
524            try {
525                nodeID = new NodeID(userOption);
526                return;
527            } catch (IllegalArgumentException e) {
528                log.error("User set node ID protocol option which is in invalid format ({}). Expected dotted hex notation like 02.01.12.FF.EE.DD", userOption);
529            }
530        }
531        List<NodeID> previous = InstanceManager.getList(NodeID.class);
532        if (!previous.isEmpty()) {
533            nodeID = previous.get(0);
534            return;
535        }
536
537        long pid = getProcessId(1);
538        log.debug("Process ID: {}", pid);
539
540        // get first network interface internet address
541        // almost certainly the wrong approach, isn't likely to
542        // find real IP address for coms, but it gets some entropy.
543        InetAddress address = null;
544        try {
545            NetworkInterface n = NetworkInterface.getNetworkInterfaces().nextElement();
546            if (n != null) {
547                address = n.getInetAddresses().nextElement();
548            }
549        } catch (SocketException | RuntimeException e) {
550            // java.util.NoSuchElementException seen on some Windows machines
551            log.warn("Can't get IP address to make NodeID", e);
552        } 
553        log.debug("InetAddress: {}", address);
554        int b1 = 0;
555        if (address != null) {
556            b1 = address.getAddress()[0];
557        }
558
559        // store new NodeID
560        nodeID = new NodeID(new byte[]{2, 1, 18, (byte) (b1 & 0xFF), (byte) ((pid >> 8) & 0xFF), (byte) (pid & 0xFF)});
561        log.debug("Node ID: {}", nodeID);
562    }
563
564    protected long getProcessId(final long fallback) {
565        // Note: may fail in some JVM implementations
566        // therefore fallback has to be provided
567
568        // something like '<pid>@<hostname>', at least in SUN / Oracle JVMs
569        final String jvmName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName();
570        final int index = jvmName.indexOf('@');
571
572        if (index < 1) {
573            // part before '@' empty (index = 0) / '@' not found (index = -1)
574            return fallback;
575        }
576
577        try {
578            return Long.parseLong(jvmName.substring(0, index));
579        } catch (NumberFormatException e) {
580            // ignore
581        }
582        return fallback;
583    }
584
585    public static CanInterface createOlcbCanInterface(NodeID nodeID, TrafficController tc) {
586        final CanInterface olcbIf = new CanInterface(nodeID, frame -> tc.sendCanMessage(convertToCan(frame), null));
587        tc.addCanListener(new CanListener() {
588            @Override
589            public void message(CanMessage m) {
590                // ignored -- loopback is handled by the olcbInterface.
591            }
592
593            @Override
594            public void reply(CanReply m) {
595                if (!m.isExtended() || m.isRtr()) {
596                    return;
597                }
598                olcbIf.frameInput().send(convertFromCan(m));
599            }
600        });
601        olcbIf.getInterface().setLoopbackThread((Runnable r)->ThreadingUtil.runOnLayout(r::run));
602        return olcbIf;
603    }
604
605    static jmri.jmrix.can.CanMessage convertToCan(org.openlcb.can.CanFrame f) {
606        jmri.jmrix.can.CanMessage fout = new jmri.jmrix.can.CanMessage(f.getData(), f.getHeader());
607        fout.setExtended(true);
608        return fout;
609    }
610
611    static OpenLcbCanFrame convertFromCan(jmri.jmrix.can.CanFrame message) {
612        OpenLcbCanFrame fin = new OpenLcbCanFrame(0);
613        fin.setHeader(message.getHeader());
614        if (message.getNumDataElements() == 0) {
615            return fin;
616        }
617        byte[] data = new byte[message.getNumDataElements()];
618        for (int i = 0; i < data.length; ++i) {
619            data[i] = (byte) (message.getElement(i) & 0xff);
620        }
621        fin.setData(data);
622        return fin;
623    }
624
625    /**
626     * State machine to handle startup
627     */
628    class StartUpHandler {
629
630        javax.swing.Timer timer;
631
632        static final int START_DELAY = 2500;
633
634        void start() {
635            log.debug("StartUpHandler starts up");
636            // wait geological time for adapter startup
637            timer = new javax.swing.Timer(START_DELAY, new javax.swing.AbstractAction() {
638
639                @Override
640                public void actionPerformed(java.awt.event.ActionEvent e) {
641                    Thread t = jmri.util.ThreadingUtil.newThread(
642                                    () -> {
643                                        // N.B. during JUnit testing, the following call tends to hang
644                                        // on semaphore acquisition in org.openlcb.can.CanInterface.initialize()
645                                        // near line 109 in openlcb lib 0.7.22, which leaves
646                                        // the thread hanging around forever.
647                                        olcbCanInterface.initialize();
648                                    },
649                                "olcbCanInterface.initialize");
650                    t.start();
651                }
652            });
653            timer.setRepeats(false);
654            timer.start();
655        }
656    }
657
658    private final static Logger log = LoggerFactory.getLogger(OlcbConfigurationManager.class);
659}