001package jmri.jmrix.openlcb; 002 003import javax.annotation.CheckReturnValue; 004import javax.annotation.Nonnull; 005import javax.annotation.OverridingMethodsMustInvokeSuper; 006 007import jmri.JmriException; 008import jmri.NamedBean; 009import jmri.implementation.AbstractStringIO; 010import jmri.jmrix.can.CanSystemConnectionMemo; 011 012import org.openlcb.Connection; 013import org.openlcb.EventID; 014import org.openlcb.MessageDecoder; 015import org.openlcb.OlcbInterface; 016import org.openlcb.ProducerConsumerEventReportMessage; 017import org.openlcb.implementations.BitProducerConsumer; 018import org.openlcb.implementations.EventTable; 019 020/** 021 * Send a message to the OpenLCB/LCC network 022 * 023 * @author Bob Jacobsen Copyright (C) 2024 024 */ 025public class OlcbStringIO extends AbstractStringIO { 026 027 OlcbAddress addrActive; // PCER address - only one! 028 029 private final OlcbInterface iface; 030 private final CanSystemConnectionMemo memo; 031 032 BitProducerConsumer pc; 033 EventTable.EventTableEntryHolder activeEventTableEntryHolder = null; 034 private static final int PC_DEFAULT_FLAGS = BitProducerConsumer.DEFAULT_FLAGS & 035 (~BitProducerConsumer.LISTEN_INVALID_STATE); 036 037 038 public OlcbStringIO(String prefix, String address, CanSystemConnectionMemo memo) { 039 super(prefix + "C" + address); 040 log.trace("ctor with {} and {}", prefix, address); 041 this.memo = memo; 042 if (memo != null) { // greatly simplify testing 043 this.iface = memo.get(OlcbInterface.class); 044 } else { 045 this.iface = null; 046 } 047 init(address); 048 } 049 050 /** 051 * Common initialization for constructor(s). 052 * <p> 053 * 054 */ 055 private void init(String address) { 056 // build local addresses 057 OlcbAddress a = new OlcbAddress(address, memo); 058 OlcbAddress[] v = a.split(memo); 059 if (v == null) { 060 log.error("Did not find usable system name: {}", address); 061 return; 062 } 063 switch (v.length) { 064 case 1: 065 addrActive = v[0]; 066 break; 067 default: 068 log.error("Can't parse OpenLCB StringIO system name: {}", address); 069 } 070 071 iface.registerMessageListener(new EWPListener()); 072 073 } 074 075 /** 076 * Helper function that will be invoked after construction once the properties have been 077 * loaded. Used specifically for preventing double initialization when loading StringIO from 078 * XML. 079 */ 080 void finishLoad() { 081 log.trace("finishLoad runs"); 082 int flags = PC_DEFAULT_FLAGS; 083 flags = OlcbUtils.overridePCFlagsFromProperties(this, flags); 084 log.debug("StringIO Flags: default {} overridden {} listen bit {}", PC_DEFAULT_FLAGS, flags, 085 BitProducerConsumer.LISTEN_EVENT_IDENTIFIED); 086 disposePc(); 087 088 pc = new BitProducerConsumer(iface, 089 addrActive.toEventID(), 090 BitProducerConsumer.nullEvent, 091 flags); 092 093 // we don't listen to the VersionedValueListener to set state 094 095 activeEventTableEntryHolder = iface.getEventTable().addEvent(addrActive.toEventID(), getEventName(true)); 096 } 097 098 private void disposePc() { 099 if (pc != null) { 100 pc.release(); 101 pc = null; 102 } 103 } 104 105 /** 106 * Computes the display name of a given event to be entered into the Event Table. 107 * @param isActive left over from interface for Turnout and Sensor, this is ignored 108 * @return user-visible string to represent this event. 109 */ 110 public String getEventName(boolean isActive) { 111 String name = getUserName(); 112 if (name == null) name = mSystemName; 113 String msgName = "StringIOEventName"; 114 return Bundle.getMessage(msgName, name); 115 } 116 117 public EventID getEventID(boolean isActive) { 118 return addrActive.toEventID(); 119 } 120 121 @Override 122 @CheckReturnValue 123 @Nonnull 124 public String getRecommendedToolTip() { 125 return addrActive.toDottedString(); 126 } 127 128 /** 129 * Updates event table entries when the user name changes. 130 * @param s new user name 131 * @throws NamedBean.BadUserNameException see {@link NamedBean} 132 */ 133 @Override 134 @OverridingMethodsMustInvokeSuper 135 public void setUserName(String s) throws NamedBean.BadUserNameException { 136 super.setUserName(s); 137 if (activeEventTableEntryHolder != null) { 138 activeEventTableEntryHolder.getEntry().updateDescription(getEventName(true)); 139 } 140 } 141 142 /** 143 * Request an update on status by sending an OpenLCB message. 144 */ 145 @Override 146 public void requestUpdateFromLayout() { 147 if (pc != null) { 148 pc.resetToDefault(); 149 pc.sendQuery(); 150 } 151 } 152 153 /** {@inheritDoc} */ 154 @Override 155 protected void sendStringToLayout(String value) throws JmriException { 156 // Does not set the known value immediately. Instead, it waits 157 // for the OpenLCB message to be received on the network, and reacts then. 158 // This is JMRI's standard MONITORING feedback. 159 160 // Send the message to the network 161 iface.getOutputConnection().put( 162 new ProducerConsumerEventReportMessage(iface.getNodeId(), 163 getEventID(true), 164 value.getBytes(java.nio.charset.StandardCharsets.UTF_8)), 165 null); 166 167 } 168 169 /** {@inheritDoc} */ 170 @Override 171 public int getMaximumLength() { 172 return 242; // Event With Payload limit 173 } 174 175 /** {@inheritDoc} */ 176 @Override 177 protected boolean cutLongStrings() { 178 return true; 179 } 180 181 class EWPListener extends MessageDecoder { 182 @Override 183 public void handleProducerConsumerEventReport(ProducerConsumerEventReportMessage msg, Connection sender){ 184 if (!msg.getEventID().equals(getEventID(true))) { 185 return; 186 } 187 // found contents, set the string on Swing thread 188 jmri.util.ThreadingUtil.runOnGUI( () -> { 189 try { 190 setString(new String(msg.getPayloadArray(), java.nio.charset.StandardCharsets.UTF_8)); 191 } catch (Exception e) { 192 log.warn("EWP processing got exception", e); 193 } 194 }); 195 } 196 } 197 198 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbStringIO.class); 199 200}