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