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 */ 279 static class StateMachine<T> extends org.openlcb.MessageDecoder { 280 public StateMachine(@Nonnull Connection connection, @Nonnull NodeID node, @Nonnull T start) { 281 this.connection = connection; 282 this.node = node; 283 this.state = start; 284 } 285 286 final Connection connection; 287 final NodeID node; 288 T state; 289 boolean initizalized = false; 290 protected final HashMap<T, String> stateToEventString = new HashMap<>(); 291 protected final HashMap<T, EventID> stateToEventID = new HashMap<>(); 292 protected final HashMap<EventID, T> eventToState = new HashMap<>(); // for efficiency, but requires no null entries 293 294 public void setState(@Nonnull T newState) { 295 log.debug("sending PCER to {}", getEventStringForState(newState)); 296 connection.put( 297 new ProducerConsumerEventReportMessage(node, getEventIDForState(newState)), 298 null); 299 } 300 301 private static final EventID nullEvent = new EventID(new byte[]{0,0,0,0,0,0,0,0}); 302 303 @Nonnull 304 public T getState() { return state; } 305 306 public void setEventForState(@Nonnull T key, @Nonnull String value) { 307 stateToEventString.put(key, value); 308 309 EventID eid = new OlcbAddress(value).toEventID(); 310 stateToEventID.put(key, eid); 311 312 // check for whether already there; so, we're done. 313 if (eventToState.get(eid) == null) { 314 // Not there yet, save it 315 eventToState.put(eid, key); 316 317 if (! nullEvent.equals(eid)) { // and if not the null (i.e. not the "don't send") event 318 // emit Producer, Consumer Identified messages to show our interest 319 connection.put( 320 new ProducerIdentifiedMessage(node, eid, EventState.Unknown), 321 null); 322 connection.put( 323 new ConsumerIdentifiedMessage(node, eid, EventState.Unknown), 324 null); 325 326 // emit Identify Producer, Consumer messages to get distributed state 327 connection.put( 328 new IdentifyProducersMessage(node, eid), 329 null); 330 connection.put( 331 new IdentifyConsumersMessage(node, eid), 332 null); 333 } 334 } 335 } 336 337 @CheckForNull 338 public EventID getEventIDForState(@Nonnull T key) { 339 EventID retval = stateToEventID.get(key); 340 if (retval == null) retval = new EventID("00.00.00.00.00.00.00.00"); 341 return retval; 342 } 343 @CheckForNull 344 public String getEventStringForState(@Nonnull T key) { 345 String retval = stateToEventString.get(key); 346 if (retval == null) retval = "00.00.00.00.00.00.00.00"; 347 return retval; 348 } 349 350 /** 351 * Internal method to determine the EventState for a reply 352 * to an Identify* method 353 * @param event Method returns the underlying state for this EventID 354 * @return State corresponding to the given EventID 355 */ 356 EventState getEventIDState(EventID event) { 357 T value = eventToState.get(event); 358 if (initizalized) { 359 if (value.equals(state)) { 360 return EventState.Valid; 361 } else { 362 return EventState.Invalid; 363 } 364 } else { 365 return EventState.Unknown; 366 } 367 } 368 369 /** 370 * {@inheritDoc} 371 */ 372 @Override 373 public void handleProducerConsumerEventReport(@Nonnull ProducerConsumerEventReportMessage msg, Connection sender){ 374 if (eventToState.containsKey(msg.getEventID())) { 375 initizalized = true; 376 state = eventToState.get(msg.getEventID()); 377 } 378 } 379 /** 380 * {@inheritDoc} 381 */ 382 @Override 383 public void handleProducerIdentified(@Nonnull ProducerIdentifiedMessage msg, Connection sender){ 384 // process if for here and marked "valid" 385 if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) { 386 initizalized = true; 387 state = eventToState.get(msg.getEventID()); 388 } 389 } 390 /** 391 * {@inheritDoc} 392 */ 393 @Override 394 public void handleConsumerIdentified(@Nonnull ConsumerIdentifiedMessage msg, Connection sender){ 395 // process if for here and marked "valid" 396 if (eventToState.containsKey(msg.getEventID()) && msg.getEventState() == EventState.Valid) { 397 initizalized = true; 398 state = eventToState.get(msg.getEventID()); 399 } 400 } 401 402 /** 403 * {@inheritDoc} 404 */ 405 @Override 406 public void handleIdentifyEventsAddressed(@Nonnull IdentifyEventsAddressedMessage msg, 407 Connection sender){ 408 // ours? 409 if (! node.equals(msg.getDestNodeID())) return; // not to us 410 sendAllIdentifiedMessages(); 411 } 412 413 /** 414 * {@inheritDoc} 415 */ 416 @Override 417 public void handleIdentifyEventsGlobal(@Nonnull IdentifyEventsGlobalMessage msg, 418 Connection sender){ 419 sendAllIdentifiedMessages(); 420 } 421 422 /** 423 * Used at start up to emit the required messages, and in response to a IdentifyEvents message 424 */ 425 public void sendAllIdentifiedMessages() { 426 // identify as consumer and producer in same pass 427 Set<Map.Entry<EventID,T>> set = eventToState.entrySet(); 428 for (Map.Entry<EventID,T> entry : set) { 429 EventID event = entry.getKey(); 430 connection.put( 431 new ConsumerIdentifiedMessage(node, event, getEventIDState(event)), 432 null); 433 connection.put( 434 new ProducerIdentifiedMessage(node, event, getEventIDState(event)), 435 null); 436 } 437 } 438 /** 439 * {@inheritDoc} 440 */ 441 @Override 442 public void handleIdentifyProducers(@Nonnull IdentifyProducersMessage msg, Connection sender){ 443 // process if we have the event 444 EventID event = msg.getEventID(); 445 if (eventToState.containsKey(event)) { 446 connection.put( 447 new ProducerIdentifiedMessage(node, event, getEventIDState(event)), 448 null); 449 } 450 } 451 /** 452 * {@inheritDoc} 453 */ 454 @Override 455 public void handleIdentifyConsumers(@Nonnull IdentifyConsumersMessage msg, Connection sender){ 456 // process if we have the event 457 EventID event = msg.getEventID(); 458 if (eventToState.containsKey(event)) { 459 connection.put( 460 new ConsumerIdentifiedMessage(node, event, getEventIDState(event)), 461 null); 462 } 463 } 464 465 } 466 467 private static final Logger log = LoggerFactory.getLogger(OlcbSignalMast.class); 468 469} 470 471