001package jmri.jmrix.openlcb; 002 003import java.util.*; 004import javax.annotation.*; 005 006import jmri.implementation.AbstractSignalMast; 007import jmri.SystemConnectionMemo; 008 009import org.openlcb.Connection; 010import org.openlcb.EventID; 011import org.openlcb.EventState; 012import org.openlcb.IdentifyEventsAddressedMessage; 013import org.openlcb.IdentifyEventsGlobalMessage; 014import org.openlcb.Message; 015import org.openlcb.MessageDecoder; 016import org.openlcb.NodeID; 017import org.openlcb.OlcbInterface; 018import org.openlcb.ProducerConsumerEventReportMessage; 019import org.openlcb.IdentifyConsumersMessage; 020import org.openlcb.ConsumerIdentifiedMessage; 021import org.openlcb.IdentifyProducersMessage; 022import org.openlcb.ProducerIdentifiedMessage; 023 024/** 025 * This class implements a SignalMast that use <B>OpenLCB Events</B> 026 * to set aspects. 027 * <p> 028 * This implementation writes out to the OpenLCB when it's commanded to 029 * change appearance, and updates its internal state when it hears Events from 030 * the network (including its own events). 031 * <p> 032 * System name specifies the creation information: 033 * <pre> 034 * IF$dsm:basic:one-searchlight(123) 035 * </pre> The name is a colon-separated series of terms: 036 * <ul> 037 * <li>I - system prefix 038 * <li>F$olm - defines signal masts of this type 039 * <li>basic - name of the signaling system 040 * <li>one-searchlight - name of the particular aspect map 041 * <li>($123) - number distinguishing this from others 042 * </ul> 043 * <p> 044 * EventIDs are returned in format in which they were provided. 045 * <p> 046 * To keep OpenLCB distributed state consistent, {@link #setAspect} does not immediately 047 * change the local aspect. Instead, it produces the relevant EventId on the 048 * network, waiting for that to return and do the local state change, notification, etc. 049 * <p> 050 * Needs to have held/unheld, lit/unlit state completed - those need to Produce and Consume events as above 051 * Based upon {@link jmri.implementation.DccSignalMast} by Kevin Dickerson 052 * 053 * @author Bob Jacobsen Copyright (c) 2017, 2018 054 */ 055public class OlcbSignalMast extends AbstractSignalMast { 056 057 public OlcbSignalMast(String sys, String user) { 058 super(sys, user); 059 configureFromName(sys); 060 } 061 062 public OlcbSignalMast(String sys) { 063 super(sys); 064 configureFromName(sys); 065 } 066 067 public OlcbSignalMast(String sys, String user, String mastSubType) { 068 super(sys, user); 069 mastType = mastSubType; 070 configureFromName(sys); 071 } 072 073 protected String mastType = "F$olm"; 074 075 StateMachine<Boolean> litMachine; 076 StateMachine<Boolean> heldMachine; 077 StateMachine<String> aspectMachine; 078 079 NodeID node; 080 Connection connection; 081 String systemPrefix; 082 083 public String getSystemPrefix() { 084 return systemPrefix; 085 } 086 087 // not sure why this is a CanSystemConnectionMemo in simulator, but it is 088 jmri.jmrix.can.CanSystemConnectionMemo systemMemo; 089 090 protected void configureFromName(String systemName) { 091 // split out the basic information 092 String[] parts = systemName.split(":"); 093 if (parts.length < 3) { 094 log.error("SignalMast system name needs at least three parts: {}",systemName); 095 throw new IllegalArgumentException("System name needs at least three parts: " + systemName); 096 } 097 if (!parts[0].endsWith(mastType)) { 098 systemPrefix = null; 099 log.warn("First part of SignalMast system name is incorrect {} : {}",systemName,mastType); 100 } else { 101 systemPrefix = parts[0].substring(0, parts[0].indexOf('$') - 1); 102 java.util.List<SystemConnectionMemo> memoList = jmri.InstanceManager.getList(SystemConnectionMemo.class); 103 104 for (SystemConnectionMemo memo : memoList) { 105 if (memo.getSystemPrefix().equals(systemPrefix)) { 106 if (memo instanceof jmri.jmrix.can.CanSystemConnectionMemo) { 107 systemMemo = (jmri.jmrix.can.CanSystemConnectionMemo) memo; 108 } else { 109 log.error("Can't create mast \"{}\" because system \"{}\" is not CanSystemConnectionMemo but rather {}" 110 ,systemName,systemPrefix,memo.getClass()); 111 } 112 break; 113 } 114 } 115 116 if (systemMemo == null) { 117 log.error("No OpenLCB connection found for system prefix \"{}\", so mast \"{}\" will not function", 118 systemPrefix,systemName); 119 } 120 } 121 String system = parts[1]; 122 String mast = parts[2]; 123 124 mast = mast.substring(0, mast.indexOf('(')); 125 setMastType(mast); 126 String tmp = parts[2].substring(parts[2].indexOf("($") + 2, parts[2].indexOf(')')); // +2 because we're looking for 2 characters 127 128 try { 129 mastNumber = Integer.parseInt(tmp); 130 if (mastNumber > lastRef) { 131 setLastRef(mastNumber); 132 } 133 } catch (NumberFormatException e) { 134 log.warn("Mast number of SystemName {} is not in the correct format: {} is not an integer", systemName, tmp); 135 } 136 configureSignalSystemDefinition(system); 137 configureAspectTable(system, mast); 138 139 if (systemMemo != null) { // initialization that requires a connection, normally present 140 node = systemMemo.get(OlcbInterface.class).getNodeId(); 141 connection = systemMemo.get(OlcbInterface.class).getOutputConnection(); 142 143 litMachine = new StateMachine<>(connection, node, Boolean.TRUE); 144 heldMachine = new StateMachine<>(connection, node, Boolean.FALSE); 145 String configureAspect = getAspect(); 146 if ( configureAspect == null ) { 147 log.debug("No Starting Aspect set for {}", getDisplayName()); 148 configureAspect = ""; 149 } 150 aspectMachine = new StateMachine<>(connection, node, configureAspect); 151 152 systemMemo.get(OlcbInterface.class).registerMessageListener(new MessageDecoder(){ 153 @Override 154 public void put(Message msg, Connection sender) { 155 handleMessage(msg); 156 } 157 }); 158 159 } 160 } 161 162 int mastNumber; // used to tell them apart 163 164 public void setOutputForAppearance(String appearance, String event) { 165 aspectMachine.setEventForState(appearance, event); 166 } 167 168 public boolean isOutputConfigured(String appearance) { 169 return aspectMachine.getEventStringForState(appearance) != null; 170 } 171 172 public String getOutputForAppearance(String appearance) { 173 String retval = aspectMachine.getEventStringForState(appearance); 174 if (retval == null) { 175 log.error("Trying to get appearance {} but it has not been configured",appearance); 176 return ""; 177 } 178 return retval; 179 } 180 181 @Override 182 public void setAspect(@Nonnull String aspect) { 183 aspectMachine.setState(aspect); 184 // Normally, the local state is changed by super.setAspect(aspect); here; see comment at top 185 } 186 187 /** 188 * Handle incoming messages. 189 * 190 * @param msg the message to handle. 191 */ 192 public void handleMessage(Message msg) { 193 // gather before state 194 Boolean litBefore = litMachine.getState(); 195 Boolean heldBefore = heldMachine.getState(); 196 String aspectBefore = aspectMachine.getState(); // before the update 197 198 // handle message 199 msg.applyTo(litMachine, null); 200 msg.applyTo(heldMachine, null); 201 msg.applyTo(aspectMachine, null); 202 203 // handle changes, if any 204 if (!litBefore.equals(litMachine.getState())) firePropertyChange("Lit", litBefore, litMachine.getState()); 205 if (!heldBefore.equals(heldMachine.getState())) firePropertyChange("Held", heldBefore, heldMachine.getState()); 206 207 this.aspect = aspectMachine.getState(); // after the update 208 this.speed = (String) getSignalSystem().getProperty(aspect, "speed"); 209 // need to check aspect != null because original getAspect (at ctor time) can return null, even though StateMachine disallows it. 210 if (aspect==null || ! aspect.equals(aspectBefore)) firePropertyChange("Aspect", aspectBefore, aspect); 211 212 } 213 214 /** 215 * Always communicates via OpenLCB 216 */ 217 @Override 218 public void setLit(boolean newLit) { 219 boolean oldLit = litMachine.getState(); 220 litMachine.setState(newLit); 221 if (!litMachine.hasEventForState(newLit) && oldLit != newLit) { 222 firePropertyChange("Lit", oldLit, newLit); 223 } 224 } 225 @Override 226 public boolean getLit() { 227 return litMachine.getState(); 228 } 229 230 /** 231 * Always communicates via OpenLCB 232 */ 233 @Override 234 public void setHeld(boolean newHeld) { 235 boolean oldHeld = heldMachine.getState(); 236 heldMachine.setState(newHeld); 237 if (!heldMachine.hasEventForState(newHeld) && oldHeld != newHeld) { 238 firePropertyChange("Held", oldHeld, newHeld); 239 } 240 } 241 @Override 242 public boolean getHeld() { 243 return heldMachine.getState(); 244 } 245 246 /** 247 * 248 * @param newVal for ordinal of all OlcbSignalMasts in use 249 */ 250 protected static void setLastRef(int newVal) { 251 lastRef = newVal; 252 } 253 254 /** 255 * Provide the last used sequence number of all OlcbSignalMasts in use. 256 * @return last used OlcbSignalMasts sequence number 257 */ 258 public static int getLastRef() { 259 return lastRef; 260 } 261 protected static volatile int lastRef = 0; 262 // TODO narrow access variable 263 //private static volatile int lastRef = 0; 264 265 public void setLitEventId(String event) { litMachine.setEventForState(Boolean.TRUE, event); } 266 public String getLitEventId() { return litMachine.getEventStringForState(Boolean.TRUE); } 267 public void setNotLitEventId(String event) { litMachine.setEventForState(Boolean.FALSE, event); } 268 public String getNotLitEventId() { return litMachine.getEventStringForState(Boolean.FALSE); } 269 270 public void setHeldEventId(String event) { heldMachine.setEventForState(Boolean.TRUE, event); } 271 public String getHeldEventId() { return heldMachine.getEventStringForState(Boolean.TRUE); } 272 public void setNotHeldEventId(String event) { heldMachine.setEventForState(Boolean.FALSE, event); } 273 public String getNotHeldEventId() { return heldMachine.getEventStringForState(Boolean.FALSE); } 274 275 /** 276 * Implement a general state machine where state transitions are 277 * associated with the production and consumption of specific events. 278 * There's a one-to-one mapping between transitions and events. 279 * EventID storage is via Strings, so that the user-visible 280 * eventID string is preserved. 281 * <p> 282 * non-static because it references systemMemo from parent class 283 */ 284 class StateMachine<T> extends org.openlcb.MessageDecoder { 285 public StateMachine(@Nonnull Connection connection, @Nonnull NodeID node, @Nonnull T start) { 286 this.connection = connection; 287 this.node = node; 288 this.state = start; 289 } 290 291 final Connection connection; 292 final NodeID node; 293 T state; 294 boolean initizalized = false; 295 protected final HashMap<T, String> stateToEventString = new HashMap<>(); 296 protected final HashMap<T, EventID> stateToEventID = new HashMap<>(); 297 protected final HashMap<EventID, T> eventToState = new HashMap<>(); // for efficiency, but requires no null entries 298 299 public void setState(@Nonnull T newState) { 300 EventID eventId = stateToEventID.get(newState); 301 if (eventId == null || nullEvent.equals(eventId)) { 302 log.debug("No event configured for state {}; not sending PCER", newState); 303 state = newState; 304 return; 305 } 306 log.debug("sending PCER to {}", getEventStringForState(newState)); 307 connection.put( 308 new ProducerConsumerEventReportMessage(node, eventId), 309 null); 310 } 311 312 public boolean hasEventForState(@Nonnull T key) { 313 EventID eventId = stateToEventID.get(key); 314 return eventId != null && !nullEvent.equals(eventId); 315 } 316 317 private final EventID nullEvent = new EventID(new byte[]{0,0,0,0,0,0,0,0}); 318 319 @Nonnull 320 public T getState() { return state; } 321 322 public void setEventForState(@Nonnull T key, @Nonnull String value) { 323 String eventString = value.trim(); 324 stateToEventString.put(key, eventString); 325 326 EventID eid = eventString.isEmpty() 327 ? nullEvent 328 : new OlcbAddress(eventString, systemMemo).toEventID(); 329 stateToEventID.put(key, eid); 330 331 // The null event means "don't send" and must not be treated as a state. 332 if (! nullEvent.equals(eid) && eventToState.get(eid) == null) { 333 // Not there yet, save it 334 eventToState.put(eid, key); 335 // emit Producer, Consumer Identified messages to show our interest 336 connection.put( 337 new ProducerIdentifiedMessage(node, eid, EventState.Unknown), 338 null); 339 connection.put( 340 new ConsumerIdentifiedMessage(node, eid, EventState.Unknown), 341 null); 342 343 // emit Identify Producer, Consumer messages to get distributed state 344 connection.put( 345 new IdentifyProducersMessage(node, eid), 346 null); 347 connection.put( 348 new IdentifyConsumersMessage(node, eid), 349 null); 350 } 351 } 352 353 @CheckForNull 354 public EventID getEventIDForState(@Nonnull T key) { 355 EventID retval = stateToEventID.get(key); 356 if (retval == null) retval = nullEvent; 357 return retval; 358 } 359 @CheckForNull 360 public String getEventStringForState(@Nonnull T key) { 361 String retval = stateToEventString.get(key); 362 if (retval == null) retval = ""; 363 return retval; 364 } 365 366 /** 367 * Internal method to determine the EventState for a reply 368 * to an Identify* method 369 * @param event Method returns the underlying state for this EventID 370 * @return State corresponding to the given EventID 371 */ 372 EventState getEventIDState(EventID event) { 373 T value = eventToState.get(event); 374 if (initizalized) { 375 if (value.equals(state)) { 376 return EventState.Valid; 377 } else { 378 return EventState.Invalid; 379 } 380 } else { 381 return EventState.Unknown; 382 } 383 } 384 385 /** 386 * {@inheritDoc} 387 */ 388 @Override 389 public void handleProducerConsumerEventReport(@Nonnull ProducerConsumerEventReportMessage msg, Connection sender){ 390 if (eventToState.containsKey(msg.getEventID())) { 391 initizalized = true; 392 state = eventToState.get(msg.getEventID()); 393 } 394 } 395 /** 396 * {@inheritDoc} 397 */ 398 @Override 399 public void handleProducerIdentified(@Nonnull ProducerIdentifiedMessage msg, Connection sender){ 400 // process if for here and marked "valid" 401 if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) { 402 initizalized = true; 403 state = eventToState.get(msg.getEventID()); 404 } 405 } 406 /** 407 * {@inheritDoc} 408 */ 409 @Override 410 public void handleConsumerIdentified(@Nonnull ConsumerIdentifiedMessage msg, Connection sender){ 411 // process if for here and marked "valid" 412 if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) { 413 initizalized = true; 414 state = eventToState.get(msg.getEventID()); 415 } 416 } 417 418 /** 419 * {@inheritDoc} 420 */ 421 @Override 422 public void handleIdentifyEventsAddressed(@Nonnull IdentifyEventsAddressedMessage msg, 423 Connection sender){ 424 // ours? 425 if (! node.equals(msg.getDestNodeID())) return; // not to us 426 sendAllIdentifiedMessages(); 427 } 428 429 /** 430 * {@inheritDoc} 431 */ 432 @Override 433 public void handleIdentifyEventsGlobal(@Nonnull IdentifyEventsGlobalMessage msg, 434 Connection sender){ 435 sendAllIdentifiedMessages(); 436 } 437 438 /** 439 * Used at start up to emit the required messages, and in response to a IdentifyEvents message 440 */ 441 public void sendAllIdentifiedMessages() { 442 // identify as consumer and producer in same pass 443 Set<Map.Entry<EventID,T>> set = eventToState.entrySet(); 444 for (Map.Entry<EventID,T> entry : set) { 445 EventID event = entry.getKey(); 446 connection.put( 447 new ConsumerIdentifiedMessage(node, event, getEventIDState(event)), 448 null); 449 connection.put( 450 new ProducerIdentifiedMessage(node, event, getEventIDState(event)), 451 null); 452 } 453 } 454 /** 455 * {@inheritDoc} 456 */ 457 @Override 458 public void handleIdentifyProducers(@Nonnull IdentifyProducersMessage msg, Connection sender){ 459 // process if we have the event 460 EventID event = msg.getEventID(); 461 if (eventToState.containsKey(event)) { 462 connection.put( 463 new ProducerIdentifiedMessage(node, event, getEventIDState(event)), 464 null); 465 } 466 } 467 /** 468 * {@inheritDoc} 469 */ 470 @Override 471 public void handleIdentifyConsumers(@Nonnull IdentifyConsumersMessage msg, Connection sender){ 472 // process if we have the event 473 EventID event = msg.getEventID(); 474 if (eventToState.containsKey(event)) { 475 connection.put( 476 new ConsumerIdentifiedMessage(node, event, getEventIDState(event)), 477 null); 478 } 479 } 480 481 } 482 483 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbSignalMast.class); 484 485}