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}