001package jmri.jmrix.cmri;
002
003import java.util.*;
004
005import javax.annotation.*;
006
007import jmri.*;
008import jmri.Manager.NameValidity;
009import jmri.jmrix.*;
010import jmri.jmrix.cmri.serial.*;
011import jmri.jmrix.cmri.swing.CMRIComponentFactory;
012import jmri.jmrix.swing.ComponentFactory;
013import jmri.util.NamedBeanComparator;
014
015/**
016 * Minimal SystemConnectionMemo for C/MRI systems.
017 *
018 * @author Randall Wood
019 */
020public class CMRISystemConnectionMemo extends DefaultSystemConnectionMemo implements ConfiguringSystemConnectionMemo {
021
022    public CMRISystemConnectionMemo() {
023        this("C", CMRIConnectionTypeList.CMRI); // default to "C" prefix
024    }
025
026    public CMRISystemConnectionMemo(@Nonnull String prefix, @Nonnull String userName) {
027        super(prefix, userName);
028
029        InstanceManager.store(this, CMRISystemConnectionMemo.class); // also register as specific type
030
031        // create and register the ComponentFactory for the GUI
032        cf = new CMRIComponentFactory(this);
033        InstanceManager.store(cf, ComponentFactory.class);
034
035        log.debug("Created CMRISystemConnectionMemo");
036    }
037
038    private String externalConfig = null;
039    private SerialTrafficController tc = null;
040    private Config config;
041    ComponentFactory cf = null;
042
043    /**
044     * Set external config for this adapter.
045     * If external config is used, the configuration is read from an external
046     * xml file which can be shared among profiles.
047     * The filename usually starts with "settings:", for example
048     * "settings:cmri_external_config.xml".
049     *
050     * @param externalConfig the filename of the config xml file or null if no
051     *                       external config
052     */
053    public void setExternalConfig(String externalConfig) {
054        this.externalConfig = externalConfig;
055    }
056
057    /**
058     * Get external config for this adapter.
059     * If external config is used, the configuration is read from an external
060     * xml file which can be shared among profiles.
061     * The filename usually starts with "settings:", for example
062     * "settings:cmri_external_config.xml".
063     *
064     * @return externalConfig the filename of the config xml file or null if no
065     *                        external config
066     */
067    public String getExternalConfig() {
068        return this.externalConfig;
069    }
070
071    /**
072     * Set the traffic controller instance associated with this connection memo.
073     *
074     * @param s jmri.jmrix.cmri.serial.SerialTrafficController object to use.
075     */
076    public void setTrafficController(SerialTrafficController s) {
077        tc = s;
078        if (config != null) {
079            restoreConfig();
080        }
081    }
082
083    /**
084     * Get the traffic controller instance associated with this connection memo.
085     *
086     * @return the traffic controller, created if needed
087     */
088    public SerialTrafficController getTrafficController() {
089        if (tc == null) {
090            setTrafficController(new SerialTrafficController());
091            log.debug("Auto create of SerialTrafficController for initial configuration");
092        }
093        return tc;
094    }
095
096    /**
097     * Get the user name for a valid system name.
098     *
099     * @param systemName the system name
100     * @return "" (null string) if the system name is not valid or does not
101     *         exist
102     */
103    public String getUserNameFromSystemName(String systemName) {
104        int offset = checkSystemPrefix(systemName);
105        if (offset < 1) {
106            return "";
107        }
108        if (systemName.length() < offset + 1) {
109            // not a valid system name for C/MRI
110            return "";
111        }
112        switch (systemName.charAt(offset)) {
113            case 'S':
114                Sensor s = InstanceManager.sensorManagerInstance().getBySystemName(systemName);
115                if (s != null) {
116                    return s.getUserName();
117                } else {
118                    return "";
119                }
120            case 'T':
121                Turnout t = InstanceManager.turnoutManagerInstance().getBySystemName(systemName);
122                if (t != null) {
123                    return t.getUserName();
124                } else {
125                    return "";
126                }
127            case 'L':
128                Light lgt = InstanceManager.lightManagerInstance().getBySystemName(systemName);
129                if (lgt != null) {
130                    return lgt.getUserName();
131                } else {
132                    return "";
133                }
134            default:
135                break;
136        }
137        // not any known sensor, light, or turnout
138        return "";
139    }
140
141    /**
142     * Get the bit number from a C/MRI system name. Bits are numbered from 1.
143     * Does not check whether that node is defined on current system.
144     *
145     * @param systemName the system name
146     * @return 0 if an error is found
147     */
148    public int getBitFromSystemName(String systemName) {
149        int offset = checkSystemPrefix(systemName);
150        if (offset < 1) {
151//            log.error("invalid system prefix in CMRI system name: {}", systemName); // fix test first
152            return 0;
153        }
154        if (validSystemNameFormat(systemName, systemName.charAt(offset)) != NameValidity.VALID) {
155            // No point in normalizing if a valid system name format is not present
156            return 0;
157        }
158        // Find the beginning of the bit number field
159        int k = 0;
160        for (int i = offset + 1; (i < systemName.length()) && (k == 0); i++) {
161            if (systemName.charAt(i) == 'B') {
162                k = i + 1;
163            }
164        }
165        int n; // bit number
166        if (k == 0) {
167            // here if 'B' not found, name must be CLnnxxx format
168            int num;
169            try {
170                num = Integer.parseInt(systemName.substring(offset + 1));
171            } catch (NumberFormatException e) {
172                log.warn("invalid character in number field of system name: {}", systemName);
173                return 0;
174            }
175            if (num > 0) {
176                n = num - ((num / 1000) * 1000);
177            } else {
178                log.warn("invalid CMRI system name: {}", systemName);
179                return 0;
180            }
181        } else { // k = position of "B" char in name
182            try {
183                n = Integer.parseInt(systemName.substring(k));
184            } catch (NumberFormatException e) {
185                log.warn("invalid character in bit number field of CMRI system name: {}", systemName);
186                return 0;
187            }
188        }
189        return n;
190    }
191
192    /**
193     * Check and skip the System Prefix string on a system name.
194     *
195     * @param systemName the system name
196     * @return offset of the 1st character past the prefix, or -1 if not valid
197     *         for this connection
198     */
199    public int checkSystemPrefix(String systemName) {
200        if (!systemName.startsWith(getSystemPrefix())) {
201            return -1;
202        }
203        return getSystemPrefix().length();
204    }
205
206    /**
207     * Test if a C/MRI output bit is free for assignment. Test is not performed
208     * if the node address or bit number is invalid.
209     *
210     * @param nAddress the node address
211     * @param bitNum   the output bit number
212     * @return "" (empty string) if the specified output bit is free for
213     *         assignment, else returns the system name of the conflicting
214     *         assignment.
215     */
216    public String isOutputBitFree(int nAddress, int bitNum) {
217        if ((nAddress < 0) || (nAddress > 127)) {
218            log.warn("invalid node address in free bit test");
219            return "";
220        }
221        if ((bitNum < 1) || (bitNum > 2048)) {
222            log.warn("invalid bit number in free bit test");
223            return "";
224        }
225        String sysName = makeSystemName("T", nAddress, bitNum);
226        Turnout t = InstanceManager.turnoutManagerInstance().getBySystemName(sysName);
227        if (t != null) {
228            return sysName;
229        }
230        String altName = convertSystemNameToAlternate(sysName);
231        t = InstanceManager.turnoutManagerInstance().getBySystemName(altName);
232        if (t != null) {
233            return altName;
234        }
235        if (bitNum > 1) {
236            sysName = makeSystemName("T", nAddress, bitNum - 1);
237            t = InstanceManager.turnoutManagerInstance().getBySystemName(sysName);
238            if (t != null) {
239                if (t.getNumberControlBits() == 2) {
240                    return sysName;
241                }
242            } else {
243                altName = convertSystemNameToAlternate(sysName);
244                t = InstanceManager.turnoutManagerInstance().getBySystemName(altName);
245                if (t != null) {
246                    if (t.getNumberControlBits() == 2) {
247                        return altName;
248                    }
249                }
250            }
251        }
252        sysName = makeSystemName("L", nAddress, bitNum);
253        Light lgt = InstanceManager.lightManagerInstance().getBySystemName(sysName);
254        if (lgt != null) {
255            return sysName;
256        }
257        altName = convertSystemNameToAlternate(sysName);
258        lgt = InstanceManager.lightManagerInstance().getBySystemName(altName);
259        if (lgt != null) {
260            return altName;
261        }
262        // not assigned to a turnout or a light
263        return "";
264    }
265
266    /**
267     * Normalize a C/MRI system name.
268     * <p>
269     * This routine is used to ensure that each system name is uniquely linked
270     * to one C/MRI bit, by removing extra zeros inserted by the user.
271     *
272     * @param systemName the system name
273     * @return "" (empty string) if the supplied system name does not have a
274     *         valid format. Otherwise a normalized name is returned in the same
275     *         format as the input name.
276     */
277    public String normalizeSystemName(String systemName) {
278        int offset = checkSystemPrefix(systemName);
279        if (offset < 1) {
280//            log.error("invalid system prefix in CMRI system name: {}", systemName); // fix test first
281            return "";
282        }
283        if (validSystemNameFormat(systemName, systemName.charAt(offset)) != NameValidity.VALID) {
284            // No point in normalizing if a valid system name format is not present
285            return "";
286        }
287        String nName;
288        String s = "";
289        int k = 0;
290        boolean noB = true;
291        for (int i = offset + 1; (i < systemName.length()) && noB; i++) {
292            if (systemName.charAt(i) == 'B') {
293                s = systemName.substring(offset + 1, i);
294                k = i + 1;
295                noB = false;
296            }
297        }
298        if (noB) {
299            int num = Integer.parseInt(systemName.substring(offset + 1));
300            int nAddress = num / 1000;
301            int bitNum = num - (nAddress * 1000);
302            nName = systemName.substring(0, offset + 1) + Integer.toString((nAddress * 1000) + bitNum);
303        } else {
304            int nAddress = Integer.parseInt(s);
305            int bitNum = Integer.parseInt(systemName.substring(k, systemName.length()));
306            nName = systemName.substring(0, offset + 1) + Integer.toString(nAddress) + "B" + Integer.toString(bitNum);
307        }
308        return nName;
309    }
310
311    /**
312     * Convert one format C/MRI system name to the alternate format.
313     *
314     * @param systemName the system name
315     * @return "" (empty string) if the supplied system name does not have a
316     *         valid format, or if there is no representation in the alternate
317     *         naming scheme
318     */
319    public String convertSystemNameToAlternate(String systemName) {
320        int offset = checkSystemPrefix(systemName);
321        if (offset < 1) {
322            log.error("invalid system prefix in CMRI system name: {}", systemName);
323            return "";
324        }
325        if (validSystemNameFormat(systemName, systemName.charAt(offset)) != NameValidity.VALID) {
326            // No point in trying if a valid system name format is not present
327            return "";
328        }
329        String altName;
330        String s = "";
331        int k = 0;
332        boolean noB = true;
333        for (int i = offset + 1; (i < systemName.length()) && noB; i++) {
334            if (systemName.charAt(i) == 'B') {
335                s = systemName.substring(offset + 1, i);
336                k = i + 1;
337                noB = false;
338            }
339        }
340        if (noB) {
341            int num = Integer.parseInt(systemName.substring(offset + 1));
342            int nAddress = num / 1000;
343            int bitNum = num - (nAddress * 1000);
344            altName = systemName.substring(0, offset + 1) + Integer.toString(nAddress) + "B" + Integer.toString(bitNum);
345        } else {
346            int nAddress = Integer.parseInt(s);
347            int bitNum = Integer.parseInt(systemName.substring(k, systemName.length()));
348            if (bitNum > 999) {
349                // bit number is out-of-range for a CLnnnxxx address
350                return "";
351            }
352            altName = systemName.substring(0, offset + 1) + Integer.toString((nAddress * 1000) + bitNum);
353        }
354        return altName;
355    }
356
357    /**
358     * Validate system name format. Does not check whether that node is defined
359     * on current system.
360     *
361     * @param systemName the system name
362     * @param type       the device type
363     * @return enum indicating current validity, which might be just as a prefix
364     */
365    public NameValidity validSystemNameFormat(@Nonnull String systemName, char type) {
366        int offset = checkSystemPrefix(systemName);
367        if (offset < 1) {
368            log.debug("invalid system prefix in CMRI system name: {}", systemName);
369            return NameValidity.INVALID;
370        }
371        if (systemName.charAt(offset) != type) {
372            log.debug("invalid type character in CMRI system name: {}", systemName);
373            return NameValidity.INVALID;
374        }
375        String s = "";
376        int k = 0;
377        boolean noB = true;
378        for (int i = offset + 1; (i < systemName.length()) && noB; i++) {
379            if (systemName.charAt(i) == 'B') {
380                s = systemName.substring(offset + 1, i);
381                k = i + 1;
382                noB = false;
383            }
384        }
385        if (noB) {
386            // This is a CLnnnxxx pattern address
387            int num;
388            try {
389                num = Integer.parseInt(systemName.substring(offset + 1));
390            } catch (NumberFormatException e) {
391                log.debug("invalid character in number field of CMRI system name: {}", systemName);
392                return NameValidity.INVALID;
393            }
394            if ((num < 1) || (num >= 128000)) {
395                log.debug("number field out of range in CMRI system name: {}", systemName);
396                return NameValidity.INVALID;
397            }
398            if ((num - ((num / 1000) * 1000)) == 0) {
399                log.debug("bit number not in range 1 - 999 in CMRI system name: {}", systemName);
400                if (systemName.length() <= offset + 6) {
401                    return NameValidity.VALID_AS_PREFIX_ONLY;
402                    // may become valid by adding 1 or more digits > 0
403                } else { // unless systemName.length() > offset + 6
404                    return NameValidity.INVALID;
405                }
406            }
407        } else {
408            // This is a CLnBxxx pattern address
409            if (s.length() == 0) {
410                log.debug("no node address before 'B' in CMRI system name: {}", systemName);
411                return NameValidity.INVALID;
412            }
413            int num;
414            try {
415                num = Integer.parseInt(s);
416            } catch (NumberFormatException e) {
417                log.debug("invalid character in node address field of CMRI system name: {}", systemName);
418                return NameValidity.INVALID;
419            }
420            if ((num < 0) || (num >= 128)) {
421                log.debug("node address field out of range in CMRI system name: {}", systemName);
422                return NameValidity.INVALID;
423            }
424            try {
425                num = Integer.parseInt(systemName.substring(k));
426            } catch (NumberFormatException e) {
427                log.debug("invalid character in bit number field of CMRI system name: {}", systemName);
428                return NameValidity.INVALID;
429            }
430            if (num == 0) {
431                return NameValidity.VALID_AS_PREFIX_ONLY;
432                // may become valid by adding 1 or more digits > 0, all zeros will be removed later so total length irrelevant
433            }
434            if ((num < 1) || (num > 2048)) {
435                log.debug("bit number field out of range in CMRI system name: {}", systemName);
436                return NameValidity.INVALID;
437            }
438        } // TODO add format check for CLnn:xxx format
439        return NameValidity.VALID;
440    }
441
442    /**
443     * Validate system name format. Does not check whether that node is defined
444     * on current system.
445     *
446     * @param systemName the system name
447     * @param type       the device type
448     * @param locale     the Locale for user messages
449     * @return systemName unmodified
450     * @throws IllegalArgumentException if unable to validate systemName
451     */
452    public String validateSystemNameFormat(String systemName, char type, Locale locale) throws IllegalArgumentException {
453        String prefix = getSystemPrefix() + type;
454        if (!systemName.startsWith(prefix)) {
455            throw new NamedBean.BadSystemNameException(
456                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidPrefix", systemName),
457                    Bundle.getMessage(locale, "InvalidSystemNameInvalidPrefix", systemName));
458        }
459        String address = systemName.substring(prefix.length());
460        int node = 0;
461        int bit = 0;
462        if (!address.contains("B") && !address.contains(":")) {
463            // This is a CLnnnxxx pattern address
464            int num;
465            try {
466                num = Integer.parseInt(address);
467                node = num / 1000;
468                bit = num - ((num / 1000) * 1000);
469            } catch (NumberFormatException ex) {
470                throw new NamedBean.BadSystemNameException(
471                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNotInteger", systemName, prefix),
472                        Bundle.getMessage(locale, "InvalidSystemNameNotInteger", systemName, prefix));
473            }
474        } else {
475            // This is a CLnBxxx or CLn:xxx pattern address
476            String[] parts = address.split("B");
477            if (parts.length != 2) {
478                parts = address.split(":");
479                if (parts.length != 2) {
480                    if (address.indexOf(":") == 0 && address.indexOf("B") == 0) {
481                        // no node
482                        throw new NamedBean.BadSystemNameException(
483                                Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNodeInvalid", systemName, ""),
484                                Bundle.getMessage(locale, "InvalidSystemNameNodeInvalid", systemName, ""));
485                    } else {
486                        // no bit
487                        throw new NamedBean.BadSystemNameException(
488                                Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBitInvalid", systemName, ""),
489                                Bundle.getMessage(locale, "InvalidSystemNameBitInvalid", systemName, ""));
490                    }
491                }
492            }
493            try {
494                node = Integer.parseInt(parts[0]);
495            } catch (NumberFormatException ex) {
496                throw new NamedBean.BadSystemNameException(
497                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNodeInvalid", systemName, parts[0]),
498                        Bundle.getMessage(locale, "InvalidSystemNameNodeInvalid", systemName, parts[0]));
499            }
500            try {
501                bit = Integer.parseInt(parts[1]);
502            } catch (NumberFormatException ex) {
503                throw new NamedBean.BadSystemNameException(
504                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBitInvalid", systemName, parts[1]),
505                        Bundle.getMessage(locale, "InvalidSystemNameBitInvalid", systemName, parts[1]));
506            }
507        }
508        if (node < 0 || node >= 128) {
509            throw new NamedBean.BadSystemNameException(
510                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameNodeInvalid", systemName, node),
511                    Bundle.getMessage(locale, "InvalidSystemNameNodeInvalid", systemName, node));
512        }
513        if (bit < 1 || bit > 2048) {
514            throw new NamedBean.BadSystemNameException(
515                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameBitInvalid", systemName, bit),
516                    Bundle.getMessage(locale, "InvalidSystemNameBitInvalid", systemName, bit));
517        }
518        return systemName;
519    }
520
521    /**
522     * Test if a C/MRI input bit is free for assignment. Test is not performed
523     * if the node address is invalid or bit number is greater than 2048.
524     *
525     * @param nAddress the address to test
526     * @param bitNum   the bit number to tests
527     * @return "" (empty string) if the specified input bit is free for
528     *         assignment, else returns the system name of the conflicting
529     *         assignment.
530     */
531    public String isInputBitFree(int nAddress, int bitNum) {
532        if ((nAddress < 0) || (nAddress > 127)) {
533            log.warn("invalid node address in free bit test");
534            return "";
535        }
536        if ((bitNum < 1) || (bitNum > 2048)) {
537            log.warn("invalid bit number in free bit test");
538            return "";
539        }
540        String sysName = makeSystemName("S", nAddress, bitNum);
541        Sensor s = InstanceManager.sensorManagerInstance().getBySystemName(sysName);
542        if (s != null) {
543            return sysName;
544        }
545        String altName = convertSystemNameToAlternate(sysName);
546        s = InstanceManager.sensorManagerInstance().getBySystemName(altName);
547        if (s != null) {
548            return altName;
549        }
550        // not assigned to a sensor
551        return "";
552    }
553
554    /**
555     * Construct a C/MRI system name from type character, node address, and bit
556     * number.
557     * <p>
558     * If the supplied character is not valid, or the node address is out of the
559     * 0 - 127 range, or the bit number is out of the 1 - 2048 range, an error
560     * message is logged and the null string "" is returned.
561     *
562     * @param type     the device type
563     * @param nAddress the address to use
564     * @param bitNum   the bit number to assign
565     * @return a system name in the CLnnnxxx, CTnnnxxx, or CSnnnxxx format if
566     *         the bit number is 1 - 999. If the bit number is 1000 - 2048, the
567     *         system name is returned in the CLnnnBxxxx, CTnnnBxxxx, or
568     *         CSnnnBxxxx format. The returned name is normalized.
569     */
570    public String makeSystemName(String type, int nAddress, int bitNum) {
571        String nName = "";
572        if ((!type.equals("S")) && (!type.equals("L")) && (!type.equals("T"))) {
573            log.error("invalid type character proposed for system name");
574            return nName;
575        }
576        if ((nAddress < 0) || (nAddress > 127)) {
577            log.warn("invalid node address proposed for system name");
578            return nName;
579        }
580        if ((bitNum < 1) || (bitNum > 2048)) {
581            log.warn("invalid bit number proposed for system name");
582            return nName;
583        }
584        if (bitNum < 1000) {
585            nName = getSystemPrefix() + type + Integer.toString((nAddress * 1000) + bitNum);
586        } else {
587            nName = getSystemPrefix() + type + Integer.toString(nAddress) + "B" + Integer.toString(bitNum);
588        }
589        return nName;
590    }
591
592    /**
593     * Get the serial node from a C/MRI system name.
594     *
595     * @param systemName the system name
596     * @param tc         the controller for the node
597     * @return the node or null if invalid systemName format or if the node is
598     *         not found
599     */
600    public AbstractNode getNodeFromSystemName(String systemName, SerialTrafficController tc) {
601        // get the node address
602        int ua;
603        ua = getNodeAddressFromSystemName(systemName);
604        if (ua == -1) {
605            return null;
606        }
607        return tc.getNodeFromAddress(ua);
608    }
609
610    /**
611     * Validate C/MRI system name for configuration. Validates node number and
612     * system prefix.
613     *
614     * @param systemName the system name to check
615     * @param type       the device type
616     * @param tc         the controller for the device
617     * @return true if system name has a valid meaning in current configuration;
618     *         otherwise false
619     */
620    public boolean validSystemNameConfig(String systemName, char type, SerialTrafficController tc) {
621        if (validSystemNameFormat(systemName, type) != NameValidity.VALID) {
622            // No point in trying if a valid system name format is not present
623            return false;
624        }
625        SerialNode node = (SerialNode) getNodeFromSystemName(systemName, tc);
626        if (node == null) {
627            // The node indicated by this system address is not present
628            return false;
629        }
630        int bit = getBitFromSystemName(systemName);
631        if ((type == 'T') || (type == 'L')) {
632            if ((bit <= 0) || (bit > (node.numOutputCards() * node.getNumBitsPerCard()))) {
633                // The bit is not valid for this defined Serial node
634                return false;
635            }
636        } else if (type == 'S') {
637            if ((bit <= 0) || (bit > (node.numInputCards() * node.getNumBitsPerCard()))) {
638                // The bit is not valid for this defined Serial node
639                return false;
640            }
641        } else {
642            log.error("Invalid type specification in validSystemNameConfig call");
643            return false;
644        }
645        // System name has passed all tests
646        return true;
647    }
648
649    /**
650     * Get the serial node address from a C/MRI system name.
651     * <p>
652     * Nodes are numbered from 0 - 127. Does not check whether that node is
653     * defined on current system.
654     *
655     * @param systemName the name containing the node
656     * @return '-1' if invalid systemName format or if the node is not found.
657     */
658    public int getNodeAddressFromSystemName(String systemName) {
659        int offset = checkSystemPrefix(systemName);
660        if (offset < 1) {
661            return -1;
662        }
663        if ((systemName.charAt(offset) != 'L') && (systemName.charAt(offset) != 'S') && (systemName.charAt(offset) != 'T')) {
664            log.error("invalid character in header field of system name: {}", systemName);
665            return -1;
666        }
667        String s = "";
668        boolean noB = true;
669        for (int i = offset + 1; (i < systemName.length()) && noB; i++) {
670            if (systemName.charAt(i) == 'B') {
671                s = systemName.substring(offset + 1, i);
672                noB = false;
673            }
674        }
675        int ua;
676        if (noB) {
677            int num = Integer.parseInt(systemName.substring(offset + 1));
678            if (num > 0) {
679                ua = num / 1000;
680            } else {
681                log.warn("invalid CMRI system name: {}", systemName);
682                return -1;
683            }
684        } else {
685            if (s.length() == 0) {
686                log.warn("no node address before 'B' in CMRI system name: {}", systemName);
687                return -1;
688            } else {
689                try {
690                    ua = Integer.parseInt(s);
691                } catch (NumberFormatException e) {
692                    log.warn("invalid character in CMRI system name: {}", systemName);
693                    return -1;
694                }
695            }
696        }
697        return ua;
698    }
699
700    /**
701     * See {@link jmri.NamedBean#compareSystemNameSuffix} for background.
702     *
703     * This is a common implementation for C/MRI Lights, Sensors and Turnouts
704     * of the comparison method.
705     * @param suffix1 suffix to compare.
706     * @param suffix2 suffix to compare.
707     * @return CMRI comparison of suffixes.
708     */
709    @CheckReturnValue
710    public static int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2) {
711
712        // extract node numbers and bit numbers
713        int node1 = 0, node2 = 0, bit1, bit2;
714        int t; // a temporary
715
716        t = suffix1.indexOf("B");
717        if (t < 0) t = suffix1.indexOf(":");
718        if (t >= 0) {
719            // alt format
720            bit1 = Integer.parseInt(suffix1.substring(t+1));
721            if (t>0) node1 = Integer.parseInt(suffix1.substring(0, t));
722        } else {
723            // std format
724            int len = suffix1.length();
725            bit1 = Integer.parseInt(suffix1.substring(Math.max(0, len-3)));
726            if (len>3) node1 = Integer.parseInt(suffix1.substring(0, len-3));
727        }
728
729        t = suffix2.indexOf("B");
730        if (t < 0) t = suffix2.indexOf(":");
731        if (t >= 0) {
732            // alt format
733            bit2 = Integer.parseInt(suffix2.substring(t+1));
734            if (t>0) node2 = Integer.parseInt(suffix2.substring(0, t));
735        } else {
736            // std format
737            int len = suffix2.length();
738            bit2 = Integer.parseInt(suffix2.substring(Math.max(0, len-3)));
739            if (len>3) node2 = Integer.parseInt(suffix2.substring(0, len-3));
740        }
741
742        if (node1 != node2 ) return Integer.signum(node1-node2);
743        return Integer.signum(bit1-bit2);
744    }
745
746    /**
747     * Configure the common managers for CMRI connections. This puts the common
748     * manager config in one place.
749     */
750    @Override
751    public void configureManagers() {
752        InstanceManager.setSensorManager(getSensorManager());
753        getTrafficController().setSensorManager(getSensorManager());
754
755        InstanceManager.setTurnoutManager(getTurnoutManager());
756
757        InstanceManager.setLightManager(getLightManager());
758        register();
759    }
760
761    public SerialTurnoutManager getTurnoutManager() {
762        if (getDisabled()) {
763            return null;
764        }
765        return (SerialTurnoutManager) classObjectMap.computeIfAbsent(TurnoutManager.class, (Class<?> c) -> new SerialTurnoutManager(this));
766    }
767
768    public SerialSensorManager getSensorManager() {
769        if (getDisabled()) {
770            return null;
771        }
772        return (SerialSensorManager) classObjectMap.computeIfAbsent(SensorManager.class, (Class<?> c) -> new SerialSensorManager(this));
773    }
774
775    public SerialLightManager getLightManager() {
776        if (getDisabled()) {
777            return null;
778        }
779        return (SerialLightManager) classObjectMap.computeIfAbsent(LightManager.class, (Class<?> c) -> new SerialLightManager(this));
780    }
781
782    @Override
783    protected ResourceBundle getActionModelResourceBundle() {
784        return ResourceBundle.getBundle("jmri.jmrix.cmri.CmriActionListBundle");
785    }
786
787    @Override
788    public <B extends NamedBean> Comparator<B> getNamedBeanComparator(Class<B> type) {
789        return new NamedBeanComparator<>();
790    }
791
792    @Override
793    public void dispose() {
794        InstanceManager.deregister(this, CMRISystemConnectionMemo.class);
795        if (cf != null) {
796            InstanceManager.deregister(cf, ComponentFactory.class);
797        }
798        super.dispose();
799    }
800
801    /**
802     * Get the configuration to be used if the user changes the connection type.
803     * @return the configuration
804     */
805    public Config getConfig() {
806        var cfg = new Config(getTrafficController());
807        cfg.setExternalConfig(getExternalConfig());
808        return cfg;
809    }
810
811    /**
812     * Set the configuration when the user has changed the connection type.
813     * @param config the configuration
814     */
815    public void setConfig(Config config) {
816        this.config = config;
817    }
818
819    /**
820     * Restore the configuration when the user has changed the connection type.
821     * This must be done after the traffic controller has been created.
822     */
823    public void restoreConfig() {
824        setExternalConfig(config.getExternalConfig());
825        for (var node : config.nodes) {
826            tc.registerNode(node);
827        }
828    }
829
830
831    /**
832     * Configuration to be used if the user changes the connection type.
833     */
834    public static class Config implements jmri.jmrix.ConnectionConfig.Config {
835
836        private String externalConfig = null;
837        private final List<AbstractNode> nodes = new ArrayList<>();
838
839        private Config(SerialTrafficController tc) {
840            int index = 0;
841            AbstractNode node;
842            while ((node = tc.getNode(index)) != null) {
843                nodes.add(node);
844                index++;
845            }
846        }
847
848        public void setExternalConfig(String externalConfig) {
849            this.externalConfig = externalConfig;
850        }
851
852        public String getExternalConfig() {
853            return this.externalConfig;
854        }
855    }
856
857
858    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CMRISystemConnectionMemo.class);
859
860}