001package jmri.jmrix.loconet.hexfile;
002
003import javax.swing.*;
004
005import jmri.*;
006import jmri.jmrix.debugthrottle.DebugThrottleManager;
007import jmri.jmrix.loconet.LnCommandStationType;
008import jmri.jmrix.loconet.LnPacketizer;
009import jmri.jmrix.loconet.LocoNetListener;
010import jmri.jmrix.loconet.LocoNetMessage;
011import jmri.managers.DefaultProgrammerManager;
012import jmri.util.JmriJFrame;
013
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Frame to inject LocoNet messages from a hex file and (optionally) mock a response to specific Discover
019 * messages. This is a sample frame that drives a test App. It controls reading from a .hex file, feeding
020 * the information to a LocoMonFrame (monitor) and connecting to a LocoGenFrame (for
021 * manually sending commands). Pane includes a checkbox to turn on simulated replies, see {@link LnHexFilePort}.
022 * Note that running a simulated LocoNet connection, {@link HexFileFrame#configure()} will substitute the
023 * {@link jmri.progdebugger.ProgDebugger} for the {@link jmri.jmrix.loconet.LnOpsModeProgrammer}
024 * overriding the readCV and writeCV methods.
025 *
026 * @author Bob Jacobsen Copyright 2001, 2002
027 * @author Egbert Broerse 2017, 2021
028 */
029public class HexFileFrame extends JmriJFrame implements LocoNetListener {
030
031    // member declarations
032    javax.swing.JButton openHexFileButton = new javax.swing.JButton();
033    javax.swing.JButton filePauseButton = new javax.swing.JButton();
034    javax.swing.JButton jButton1 = new javax.swing.JButton();
035    javax.swing.JTextField delayField = new javax.swing.JTextField(5);
036    javax.swing.JLabel jLabel1 = new javax.swing.JLabel();
037    JCheckBox simReplyBox = new JCheckBox(Bundle.getMessage("SimReplyBox"));
038
039    private int maxSlots = 10;  //maximum addresses that can be acquired at once, this default will be overridden by config
040    private int slotsInUse = 0;
041
042    // to find and remember the log file
043    final javax.swing.JFileChooser inputFileChooser;
044
045    /**
046     * Because this creates a FileChooser, this should be invoked on the
047     * GUI frame.
048     */
049    @InvokeOnGuiThread
050    public HexFileFrame() {
051        super();
052        inputFileChooser = jmri.jmrit.XmlFile.userFileChooser("Hex files", "hex"); // NOI18N
053    }
054
055    /**
056     * {@inheritDoc}
057     */
058    @InvokeOnGuiThread
059    @Override
060    public void initComponents() {
061        if (port == null) {
062            log.error("initComponents called before adapter has been set");
063        }
064        // the following code sets the frame's initial state
065
066        openHexFileButton.setText(Bundle.getMessage("OpenFile"));
067        openHexFileButton.setVisible(true);
068        openHexFileButton.setToolTipText(Bundle.getMessage("OpenFileTooltip"));
069
070        filePauseButton.setText(Bundle.getMessage("ButtonPause"));
071        filePauseButton.setVisible(true);
072        filePauseButton.setToolTipText(Bundle.getMessage("ButtonPauseTooltip"));
073
074        jButton1.setText(Bundle.getMessage("ButtonContinue"));
075        jButton1.setVisible(true);
076        jButton1.setToolTipText(Bundle.getMessage("ButtonContinueTooltip"));
077
078        delayField.setText("200");
079        delayField.setVisible(true);
080        delayField.setToolTipText(Bundle.getMessage("DelayTooltip"));
081        delayField.addPropertyChangeListener(this::delayFieldActionPerformed);
082        
083        jLabel1.setText(Bundle.getMessage("FieldDelay"));
084        jLabel1.setVisible(true);
085
086        simReplyBox.setToolTipText(Bundle.getMessage("SimReplyTip"));
087        setTitle(Bundle.getMessage("TitleLocoNetSimulator", getAdapter().getUserName()));
088        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
089
090        JPanel pane1 = new JPanel();
091        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
092        pane1.add(openHexFileButton);
093        pane1.add(new JPanel()); // dummy
094        getContentPane().add(pane1);
095
096        JPanel pane2 = new JPanel();
097        pane2.setLayout(new BoxLayout(pane2, BoxLayout.X_AXIS));
098        pane2.add(jLabel1);
099        pane2.add(delayField);
100        getContentPane().add(pane2);
101
102        JPanel pane3 = new JPanel();
103        pane3.setLayout(new BoxLayout(pane3, BoxLayout.X_AXIS));
104        pane3.add(filePauseButton);
105        pane3.add(jButton1);
106        getContentPane().add(pane3);
107
108        JPanel pane4 = new JPanel();
109        pane4.add(simReplyBox);
110        getContentPane().add(pane4);
111        InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefMgr) -> {
112            simReplyBox.setSelected(prefMgr.getSimplePreferenceState("simReply"));
113            port.simReply(simReplyBox.isSelected()); // update state in adapter
114        });
115
116        openHexFileButton.addActionListener(this::openHexFileButtonActionPerformed);
117        filePauseButton.addActionListener(this::filePauseButtonActionPerformed);
118        jButton1.addActionListener(this::jButton1ActionPerformed);
119        simReplyBox.addActionListener(this::simReplyActionPerformed);
120
121        pack();
122    }
123
124    boolean connected = false;
125
126    @Override
127    @InvokeOnGuiThread
128    public void dispose() {
129        // leaves the LocoNet Packetizer (e.g. the simulated connection) running
130        // so that the application can keep pretending to run with the window closed.
131        super.dispose();
132    }
133
134    LnPacketizer packets = null;
135
136    @InvokeOnGuiThread
137    public void openHexFileButtonActionPerformed(java.awt.event.ActionEvent e) {
138        // select the file
139        // start at current file, show dialog
140        inputFileChooser.rescanCurrentDirectory();
141        int retVal = inputFileChooser.showOpenDialog(this);
142
143        // handle selection or cancel
144        if (retVal != JFileChooser.APPROVE_OPTION) {
145            return;  // give up if no file selected
146        }
147        // call load to process the file
148        Integer delayValue = null;
149        port.load(inputFileChooser.getSelectedFile());
150        try {
151            delayValue = Integer.parseInt(delayField.getText());
152
153        } catch (NumberFormatException exception) {
154            log.error("invalid number in delay field - {}", delayField.getText());
155        }
156        if (delayValue != null && delayValue > 1 )
157           port.setDelay(delayValue);
158        // wake copy
159        sourceThread.interrupt();  // really should be using notifyAll instead....
160
161        // reach here while file runs.  Need to return so GUI still acts,
162        // but that normally lets the button go back to default.
163    }
164
165    @InvokeOnGuiThread
166    public void configure() {
167        if (port == null) {
168            log.error("configure called before adapter has been set");
169            return;
170        }
171        // connect to a packetizing LnTrafficController
172        packets = new LnPacketizer(port.getSystemConnectionMemo());
173        packets.connectPort(port);
174        connected = true;
175
176        // create memo
177        port.getSystemConnectionMemo().setLnTrafficController(packets);
178
179        // do the common manager config
180        port.getSystemConnectionMemo().configureCommandStation(LnCommandStationType.COMMAND_STATION_DCS100, // full featured by default
181                false, false, false, false, false);
182        port.getSystemConnectionMemo().configureManagers();
183        jmri.SensorManager sm = port.getSystemConnectionMemo().getSensorManager();
184        if (sm != null) {
185            if ( sm instanceof LnSensorManager) {
186                ((LnSensorManager) sm).setDefaultSensorState(port.getOptionState("SensorDefaultState")); // NOI18N
187            } else {
188                log.info("SensorManager referenced by port is not an LnSensorManager. Have not set the default sensor state.");
189            }
190        }
191        //get the maxSlots value from the connection options
192        try {
193            maxSlots = Integer.parseInt(port.getOptionState("MaxSlots"));
194        } catch (NumberFormatException e) {
195            //ignore missing or invalid option and leave at the default value
196        }
197
198        // Install a debug programmer, replacing the existing LocoNet one
199        // Note that this needs to be repeated for the DefaultManagers, if one is set to HexFile (Ln Sim)
200        // see jmri.jmrix.loconet.hexfile.HexFileSystemConnectionMemo
201        log.debug("HexFileFrame called");
202        DefaultProgrammerManager ep = port.getSystemConnectionMemo().getProgrammerManager();
203        port.getSystemConnectionMemo().setProgrammerManager(
204                new jmri.progdebugger.DebugProgrammerManager(port.getSystemConnectionMemo()));
205        if (port.getSystemConnectionMemo().getProgrammerManager().isAddressedModePossible()) {
206            log.debug("replacing AddressedProgrammer in Hex");
207            jmri.InstanceManager.store(port.getSystemConnectionMemo().getProgrammerManager(), jmri.AddressedProgrammerManager.class);
208        }
209        if (port.getSystemConnectionMemo().getProgrammerManager().isGlobalProgrammerAvailable()) {
210            log.debug("replacing GlobalProgrammer in Hex");
211            jmri.InstanceManager.store(port.getSystemConnectionMemo().getProgrammerManager(), GlobalProgrammerManager.class);
212        }
213        jmri.InstanceManager.deregister(ep, jmri.AddressedProgrammerManager.class);
214        jmri.InstanceManager.deregister(ep, jmri.GlobalProgrammerManager.class);
215
216        // Install a debug throttle manager and override
217        DebugThrottleManager tm = new DebugThrottleManager(port.getSystemConnectionMemo() ) {
218            /**
219             * Only address 128 and above can be a long address
220             */
221            @Override
222            public boolean canBeLongAddress(int address) {
223                return (address >= 128);
224            }
225
226            @Override
227            public void requestThrottleSetup(LocoAddress a, boolean control) {
228                if (!(a instanceof DccLocoAddress)) {
229                    log.error("{} is not a DccLocoAddress",a);
230                    failedThrottleRequest(a, "LocoAddress " + a + " is not a DccLocoAddress");
231                    return;
232                }
233                DccLocoAddress address = (DccLocoAddress) a;
234
235                //check for slot limit exceeded
236                if (slotsInUse >= maxSlots) {
237                    log.warn("SLOT MAX of {} reached. Throttle {} not added. Current slotsInUse={}", maxSlots, a, slotsInUse);
238                    failedThrottleRequest(address, "SLOT MAX of " + maxSlots + " reached");
239                    return;
240                }
241
242                slotsInUse++;
243                log.debug("Throttle {} requested. slotsInUse={}, maxSlots={}", a, slotsInUse, maxSlots);
244                super.requestThrottleSetup(a, control);
245            }
246
247            @Override
248            public boolean disposeThrottle(DccThrottle t, jmri.ThrottleListener l) {
249                if (slotsInUse > 0) slotsInUse--;
250                log.debug("Throttle {} disposed. slotsInUse={}, maxSlots={}", t, slotsInUse, maxSlots);
251                return super.disposeThrottle(t, l);
252            }
253        };
254
255        port.getSystemConnectionMemo().setThrottleManager(tm);
256        jmri.InstanceManager.setThrottleManager(
257                port.getSystemConnectionMemo().getThrottleManager());
258
259        // start listening for messages
260        port.getSystemConnectionMemo().getLnTrafficController().addLocoNetListener(~0, this);
261
262        // start operation of packetizer
263        packets.startThreads();
264        sourceThread = jmri.util.ThreadingUtil.newThread(port, "LocoNet HexFileFrame");
265        sourceThread.start();
266    }
267
268    public void filePauseButtonActionPerformed(java.awt.event.ActionEvent e) {
269        port.suspendReading(true);
270    }
271
272    public void jButton1ActionPerformed(java.awt.event.ActionEvent e) {  // resume button
273        port.suspendReading(false);
274    }
275
276    public void delayFieldActionPerformed(java.beans.PropertyChangeEvent e) {
277        // if the hex file has been started, change its delay
278        if (port != null) {
279            port.setDelay(Integer.parseInt(delayField.getText()));
280        }
281    }
282
283    @Override
284    public synchronized void message(LocoNetMessage m) {
285        //log.debug("HexFileFrame heard message {}", m.toMonitorString());
286        if (port.simReply()) {
287            LocoNetMessage reply = LnHexFilePort.generateReply(m);
288            if (reply != null) {
289                packets.sendLocoNetMessage(reply);
290                //log.debug("message reply forwarded to port");
291            }
292        }
293    }
294
295    Thread sourceThread;  // tests need access
296
297    public void setAdapter(LnHexFilePort adapter) {
298        port = adapter;
299    }
300
301    public LnHexFilePort getAdapter() {
302        return port;
303    }
304    private LnHexFilePort port = null;
305
306    public void simReplyActionPerformed(java.awt.event.ActionEvent e) {  // resume button
307        port.simReply(simReplyBox.isSelected());
308        InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefMgr) -> {
309            prefMgr.setSimplePreferenceState("simReply", simReplyBox.isSelected());
310        });
311    }
312
313    private final static Logger log = LoggerFactory.getLogger(HexFileFrame.class);
314
315}