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