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}