001package jmri.jmrit.logixng.actions;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006import java.util.concurrent.atomic.AtomicInteger;
007
008import jmri.*;
009import jmri.jmrit.logixng.*;
010import jmri.jmrit.logixng.implementation.DefaultSymbolTable;
011import jmri.jmrit.logixng.util.LogixNG_SelectComboBox;
012import jmri.jmrit.logixng.util.LogixNG_SelectInteger;
013
014/**
015 * Program a CV on main.
016 *
017 * @author Daniel Bergqvist Copyright 2024
018 */
019public class ProgramOnMain extends AbstractDigitalAction
020        implements FemaleSocketListener, PropertyChangeListener {
021
022    private static final ResourceBundle rbx =
023            ResourceBundle.getBundle("jmri.jmrit.logixng.implementation.ImplementationBundle");
024
025    private String _executeSocketSystemName;
026    private final FemaleDigitalActionSocket _executeSocket;
027    private SystemConnectionMemo _memo;
028    private AddressedProgrammerManager _programmerManager;
029    private ThrottleManager _throttleManager;
030    private final LogixNG_SelectComboBox _selectProgrammingMode;
031    private final LogixNG_SelectInteger _selectAddress = new LogixNG_SelectInteger(this, this);
032    private final LogixNG_SelectInteger _selectCV = new LogixNG_SelectInteger(this, this);
033    private final LogixNG_SelectInteger _selectValue = new LogixNG_SelectInteger(this, this);
034    private String _localVariableForStatus = "";
035    private final InternalFemaleSocket _internalSocket = new InternalFemaleSocket();
036
037    public ProgramOnMain(String sys, String user) {
038        super(sys, user);
039
040        // The array is updated with correct values when setMemo() is called
041        String[] modes = {""};
042        _selectProgrammingMode = new LogixNG_SelectComboBox(this, modes, modes[0], this);
043
044        // Set the _programmerManager and _throttleManager variables
045        setMemo(null);
046
047        _executeSocket = InstanceManager.getDefault(DigitalActionManager.class)
048                .createFemaleSocket(this, this, Bundle.getMessage("ProgramOnMain_Socket"));
049    }
050
051    @Override
052    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
053        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
054        String sysName = systemNames.get(getSystemName());
055        String userName = userNames.get(getSystemName());
056        if (sysName == null) sysName = manager.getAutoSystemName();
057        ProgramOnMain copy = new ProgramOnMain(sysName, userName);
058        copy.setComment(getComment());
059        copy.setMemo(_memo);
060        _selectProgrammingMode.copy(copy._selectProgrammingMode);
061        _selectAddress.copy(copy._selectAddress);
062        _selectCV.copy(copy._selectCV);
063        _selectValue.copy(copy._selectValue);
064        copy.setLocalVariableForStatus(_localVariableForStatus);
065        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
066    }
067
068    public final LogixNG_SelectComboBox getSelectProgrammingMode() {
069        return _selectProgrammingMode;
070    }
071
072    public final LogixNG_SelectInteger getSelectAddress() {
073        return _selectAddress;
074    }
075
076    public final LogixNG_SelectInteger getSelectCV() {
077        return _selectCV;
078    }
079
080    public final LogixNG_SelectInteger getSelectValue() {
081        return _selectValue;
082    }
083
084    public void setLocalVariableForStatus(String localVariable) {
085        _localVariableForStatus = localVariable;
086    }
087
088    public String getLocalVariableForStatus() {
089        return _localVariableForStatus;
090    }
091
092    public final void setMemo(SystemConnectionMemo memo) {
093        assertListenersAreNotRegistered(log, "setMemo");
094
095        _memo = memo;
096        if (_memo != null) {
097            _programmerManager = _memo.get(AddressedProgrammerManager.class);
098            _throttleManager = _memo.get(ThrottleManager.class);
099            if (_throttleManager == null) {
100                throw new IllegalArgumentException("Memo "+memo.getUserName()+" doesn't have a ThrottleManager");
101            }
102
103            // LocoNet memo doesn't have a programmer during tests
104            if (_programmerManager == null) {
105                _programmerManager = InstanceManager.getDefault(AddressedProgrammerManager.class);
106            }
107        } else {
108            _programmerManager = InstanceManager.getDefault(AddressedProgrammerManager.class);
109            _throttleManager = InstanceManager.getDefault(ThrottleManager.class);
110        }
111
112        List<String> modeList = new ArrayList<>();
113        for (ProgrammingMode mode : _programmerManager.getDefaultModes()) {
114            log.debug("Available programming mode: {}", mode);
115            modeList.add(mode.getStandardName());
116        }
117
118        // Add OPSBYTEMODE in case we don't have any mode,
119        // for example if we are running a simulator.
120        if (modeList.isEmpty()) {
121            modeList.add(ProgrammingMode.OPSBYTEMODE.getStandardName());
122        }
123
124        String[] modes = modeList.toArray(String[]::new);
125        _selectProgrammingMode.setValues(modes);
126    }
127
128    public final SystemConnectionMemo getMemo() {
129        return _memo;
130    }
131
132    /** {@inheritDoc} */
133    @Override
134    public Category getCategory() {
135        return Category.ITEM;
136    }
137
138    private void doProgrammingOnMain(ConditionalNG conditionalNG,
139            DefaultSymbolTable newSymbolTable, ProgrammingMode progMode,
140            int address, int cv, int value)
141            throws JmriException {
142        try {
143            AddressedProgrammer programmer = _programmerManager.getAddressedProgrammer(
144                    new DccLocoAddress(address, !_throttleManager.canBeShortAddress(address)));
145
146            if (programmer != null) {
147                programmer.setMode(progMode);
148                if (!progMode.equals(programmer.getMode())) {
149                    throw new IllegalArgumentException("The addressed programmer doesn't support mode " + progMode.getStandardName());
150                }
151                AtomicInteger result = new AtomicInteger(-1);
152                programmer.writeCV("" + cv, value, (int value1, int status) -> {
153                    result.set(status);
154
155                    log.debug("Result of programming cv {} to value {} for address {}: {}", cv, value, address, status);
156
157                    synchronized(ProgramOnMain.this) {
158                        _internalSocket.conditionalNG = conditionalNG;
159                        _internalSocket.newSymbolTable = newSymbolTable;
160                        _internalSocket.status = status;
161                        conditionalNG.execute(_internalSocket);
162                    }
163                });
164
165                try {
166                    while (result.get() == -1) {
167                        Thread.sleep(10);
168                    }
169                } catch (InterruptedException e) {
170                    log.warn("Waiting for programmer to complete was aborted");
171                }
172
173            } else {
174                throw new IllegalArgumentException("An addressed programmer isn't available for address " + address);
175            }
176        } catch (ProgrammerException e) {
177            throw new JmriException(e);
178        }
179    }
180
181    /** {@inheritDoc} */
182    @Override
183    public void execute() throws JmriException {
184        ConditionalNG conditionalNG = this.getConditionalNG();
185        DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(conditionalNG.getSymbolTable());
186
187        String progModeStr = _selectProgrammingMode.evaluateValue(conditionalNG);
188        ProgrammingMode progMode = new ProgrammingMode(progModeStr);
189
190        int address = _selectAddress.evaluateValue(conditionalNG);
191        int cv = _selectCV.evaluateValue(conditionalNG);
192        int value = _selectValue.evaluateValue(conditionalNG);
193
194        doProgrammingOnMain(conditionalNG, newSymbolTable, progMode, address, cv, value);
195    }
196
197    @Override
198    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
199        switch (index) {
200            case 0:
201                return _executeSocket;
202
203            default:
204                throw new IllegalArgumentException(
205                        String.format("index has invalid value: %d", index));
206        }
207    }
208
209    @Override
210    public int getChildCount() {
211        return 1;
212    }
213
214    @Override
215    public void connected(FemaleSocket socket) {
216        if (socket == _executeSocket) {
217            _executeSocketSystemName = socket.getConnectedSocket().getSystemName();
218        } else {
219            throw new IllegalArgumentException("unkown socket");
220        }
221    }
222
223    @Override
224    public void disconnected(FemaleSocket socket) {
225        if (socket == _executeSocket) {
226            _executeSocketSystemName = null;
227        } else {
228            throw new IllegalArgumentException("unkown socket");
229        }
230    }
231
232    @Override
233    public String getShortDescription(Locale locale) {
234        return Bundle.getMessage(locale, "ProgramOnMain_Short");
235    }
236
237    @Override
238    public String getLongDescription(Locale locale) {
239        if (_memo != null) {
240            return Bundle.getMessage(locale, "ProgramOnMain_LongConnection",
241                    _selectAddress.getDescription(locale, false),
242                    _selectCV.getDescription(locale, false),
243                    _selectValue.getDescription(locale, false),
244                    _selectProgrammingMode.getDescription(locale),
245                    _memo.getUserName());
246        } else {
247            return Bundle.getMessage(locale, "ProgramOnMain_Long",
248                    _selectAddress.getDescription(locale, false),
249                    _selectCV.getDescription(locale, false),
250                    _selectValue.getDescription(locale, false),
251                    _selectProgrammingMode.getDescription(locale));
252        }
253    }
254
255    public FemaleDigitalActionSocket getExecuteSocket() {
256        return _executeSocket;
257    }
258
259    public String getExecuteSocketSystemName() {
260        return _executeSocketSystemName;
261    }
262
263    public void setExecuteSocketSystemName(String systemName) {
264        _executeSocketSystemName = systemName;
265    }
266
267    /** {@inheritDoc} */
268    @Override
269    public void setup() {
270        try {
271            if (!_executeSocket.isConnected()
272                    || !_executeSocket.getConnectedSocket().getSystemName()
273                            .equals(_executeSocketSystemName)) {
274
275                String socketSystemName = _executeSocketSystemName;
276
277                _executeSocket.disconnect();
278
279                if (socketSystemName != null) {
280                    MaleSocket maleSocket =
281                            InstanceManager.getDefault(DigitalActionManager.class)
282                                    .getBySystemName(socketSystemName);
283                    if (maleSocket != null) {
284                        _executeSocket.connect(maleSocket);
285                        maleSocket.setup();
286                    } else {
287                        log.error("cannot load digital action {}", socketSystemName);
288                    }
289                }
290            } else {
291                _executeSocket.getConnectedSocket().setup();
292            }
293        } catch (SocketAlreadyConnectedException ex) {
294            // This shouldn't happen and is a runtime error if it does.
295            throw new RuntimeException("socket is already connected");
296        }
297    }
298
299    /** {@inheritDoc} */
300    @Override
301    public void propertyChange(PropertyChangeEvent evt) {
302        getConditionalNG().execute();
303    }
304
305
306    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
307
308        private ConditionalNG conditionalNG;
309        private SymbolTable newSymbolTable;
310        private int status;
311
312        public InternalFemaleSocket() {
313            super(null, new FemaleSocketListener(){
314                @Override
315                public void connected(FemaleSocket socket) {
316                    // Do nothing
317                }
318
319                @Override
320                public void disconnected(FemaleSocket socket) {
321                    // Do nothing
322                }
323            }, "A");
324        }
325
326        @Override
327        public void execute() throws JmriException {
328            if (_executeSocket != null) {
329                MaleSocket maleSocket = (MaleSocket)ProgramOnMain.this.getParent();
330                try {
331                    SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
332                    conditionalNG.setSymbolTable(newSymbolTable);
333                    if (!_localVariableForStatus.isEmpty()) {
334                        newSymbolTable.setValue(_localVariableForStatus, status);
335                    }
336                    _executeSocket.execute();
337                    conditionalNG.setSymbolTable(oldSymbolTable);
338                } catch (JmriException e) {
339                    if (e.getErrors() != null) {
340                        maleSocket.handleError(ProgramOnMain.this, rbx.getString("ExceptionExecuteMulti"), e.getErrors(), e, log);
341                    } else {
342                        maleSocket.handleError(ProgramOnMain.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
343                    }
344                } catch (RuntimeException e) {
345                    maleSocket.handleError(ProgramOnMain.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
346                }
347            }
348        }
349
350    }
351
352
353    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProgramOnMain.class);
354
355}