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
082        jLabel1.setText(Bundle.getMessage("FieldDelay"));
083        jLabel1.setVisible(true);
084
085        simReplyBox.setToolTipText(Bundle.getMessage("SimReplyTip"));
086        setTitle(Bundle.getMessage("TitleLocoNetSimulator", getAdapter().getUserName()));
087        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
088
089        JPanel pane1 = new JPanel();
090        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
091        pane1.add(openHexFileButton);
092        pane1.add(new JPanel()); // dummy
093        getContentPane().add(pane1);
094
095        JPanel pane2 = new JPanel();
096        pane2.setLayout(new BoxLayout(pane2, BoxLayout.X_AXIS));
097        pane2.add(jLabel1);
098        pane2.add(delayField);
099        getContentPane().add(pane2);
100
101        JPanel pane3 = new JPanel();
102        pane3.setLayout(new BoxLayout(pane3, BoxLayout.X_AXIS));
103        pane3.add(filePauseButton);
104        pane3.add(jButton1);
105        getContentPane().add(pane3);
106
107        JPanel pane4 = new JPanel();
108        pane4.add(simReplyBox);
109        getContentPane().add(pane4);
110        InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefMgr) -> {
111            simReplyBox.setSelected(prefMgr.getSimplePreferenceState("simReply"));
112            port.simReply(simReplyBox.isSelected()); // update state in adapter
113        });
114
115        openHexFileButton.addActionListener(this::openHexFileButtonActionPerformed);
116        filePauseButton.addActionListener(this::filePauseButtonActionPerformed);
117        jButton1.addActionListener(this::jButton1ActionPerformed);
118        delayField.addActionListener(this::delayFieldActionPerformed);
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        port.load(inputFileChooser.getSelectedFile());
149
150        // wake copy
151        sourceThread.interrupt();  // really should be using notifyAll instead....
152
153        // reach here while file runs.  Need to return so GUI still acts,
154        // but that normally lets the button go back to default.
155    }
156
157    @InvokeOnGuiThread
158    public void configure() {
159        if (port == null) {
160            log.error("configure called before adapter has been set");
161            return;
162        }
163        // connect to a packetizing LnTrafficController
164        packets = new LnPacketizer(port.getSystemConnectionMemo());
165        packets.connectPort(port);
166        connected = true;
167
168        // create memo
169        port.getSystemConnectionMemo().setLnTrafficController(packets);
170
171        // do the common manager config
172        port.getSystemConnectionMemo().configureCommandStation(LnCommandStationType.COMMAND_STATION_DCS100, // full featured by default
173                false, false, false);
174        port.getSystemConnectionMemo().configureManagers();
175        jmri.SensorManager sm = port.getSystemConnectionMemo().getSensorManager();
176        if (sm != null) {
177            if ( sm instanceof LnSensorManager) {
178                ((LnSensorManager) sm).setDefaultSensorState(port.getOptionState("SensorDefaultState")); // NOI18N
179            } else {
180                log.info("SensorManager referenced by port is not an LnSensorManager. Have not set the default sensor state.");
181            }
182        }
183        //get the maxSlots value from the connection options
184        try {
185            maxSlots = Integer.parseInt(port.getOptionState("MaxSlots"));
186        } catch (NumberFormatException e) {
187            //ignore missing or invalid option and leave at the default value
188        }
189
190        // Install a debug programmer, replacing the existing LocoNet one
191        // Note that this needs to be repeated for the DefaultManagers, if one is set to HexFile (Ln Sim)
192        // see jmri.jmrix.loconet.hexfile.HexFileSystemConnectionMemo
193        log.debug("HexFileFrame called");
194        DefaultProgrammerManager ep = port.getSystemConnectionMemo().getProgrammerManager();
195        port.getSystemConnectionMemo().setProgrammerManager(
196                new jmri.progdebugger.DebugProgrammerManager(port.getSystemConnectionMemo()));
197        if (port.getSystemConnectionMemo().getProgrammerManager().isAddressedModePossible()) {
198            log.debug("replacing AddressedProgrammer in Hex");
199            jmri.InstanceManager.store(port.getSystemConnectionMemo().getProgrammerManager(), jmri.AddressedProgrammerManager.class);
200        }
201        if (port.getSystemConnectionMemo().getProgrammerManager().isGlobalProgrammerAvailable()) {
202            log.debug("replacing GlobalProgrammer in Hex");
203            jmri.InstanceManager.store(port.getSystemConnectionMemo().getProgrammerManager(), GlobalProgrammerManager.class);
204        }
205        jmri.InstanceManager.deregister(ep, jmri.AddressedProgrammerManager.class);
206        jmri.InstanceManager.deregister(ep, jmri.GlobalProgrammerManager.class);
207
208        // Install a debug throttle manager and override 
209        DebugThrottleManager tm = new DebugThrottleManager(port.getSystemConnectionMemo() ) {
210            /**
211             * Only address 128 and above can be a long address
212             */
213            @Override
214            public boolean canBeLongAddress(int address) {
215                return (address >= 128);
216            }
217
218            @Override
219            public void requestThrottleSetup(LocoAddress a, boolean control) {
220                if (!(a instanceof DccLocoAddress)) {
221                    log.error("{} is not a DccLocoAddress",a);
222                    failedThrottleRequest(a, "LocoAddress " + a + " is not a DccLocoAddress");
223                    return;
224                }
225                DccLocoAddress address = (DccLocoAddress) a;
226
227                //check for slot limit exceeded
228                if (slotsInUse >= maxSlots) {
229                    log.warn("SLOT MAX of {} reached. Throttle {} not added. Current slotsInUse={}", maxSlots, a, slotsInUse);
230                    failedThrottleRequest(address, "SLOT MAX of " + maxSlots + " reached");
231                    return;
232                }
233
234                slotsInUse++;
235                log.debug("Throttle {} requested. slotsInUse={}, maxSlots={}", a, slotsInUse, maxSlots);
236                super.requestThrottleSetup(a, control);
237            }
238
239            @Override
240            public boolean disposeThrottle(DccThrottle t, jmri.ThrottleListener l) {                
241                if (slotsInUse > 0) slotsInUse--;
242                log.debug("Throttle {} disposed. slotsInUse={}, maxSlots={}", t, slotsInUse, maxSlots);
243                return super.disposeThrottle(t, l);
244            }    
245        };
246
247        port.getSystemConnectionMemo().setThrottleManager(tm);
248        jmri.InstanceManager.setThrottleManager(
249                port.getSystemConnectionMemo().getThrottleManager());
250
251        // start listening for messages
252        port.getSystemConnectionMemo().getLnTrafficController().addLocoNetListener(~0, this);
253
254        // start operation of packetizer
255        packets.startThreads();
256        sourceThread = new Thread(port, "LocoNet HexFileFrame");
257        sourceThread.start();
258    }
259
260    @SuppressWarnings("deprecation")  // Thread.suspend() not being removed
261    public void filePauseButtonActionPerformed(java.awt.event.ActionEvent e) {
262        sourceThread.suspend();
263    }
264
265    @SuppressWarnings("deprecation")  // Thread.resume() not being removed
266    public void jButton1ActionPerformed(java.awt.event.ActionEvent e) {  // resume button
267        sourceThread.resume();
268    }
269
270    public void delayFieldActionPerformed(java.awt.event.ActionEvent e) {
271        // if the hex file has been started, change its delay
272        if (port != null) {
273            port.setDelay(Integer.parseInt(delayField.getText()));
274        }
275    }
276
277    @Override
278    public synchronized void message(LocoNetMessage m) {
279        //log.debug("HexFileFrame heard message {}", m.toMonitorString());
280        if (port.simReply()) {
281            LocoNetMessage reply = LnHexFilePort.generateReply(m);
282            if (reply != null) {
283                packets.sendLocoNetMessage(reply);
284                //log.debug("message reply forwarded to port");
285            }
286        }
287    }
288
289    Thread sourceThread;  // tests need access
290
291    public void setAdapter(LnHexFilePort adapter) {
292        port = adapter;
293    }
294
295    public LnHexFilePort getAdapter() {
296        return port;
297    }
298    private LnHexFilePort port = null;
299
300    public void simReplyActionPerformed(java.awt.event.ActionEvent e) {  // resume button
301        port.simReply(simReplyBox.isSelected());
302        InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefMgr) -> {
303            prefMgr.setSimplePreferenceState("simReply", simReplyBox.isSelected());
304        });
305    }
306
307    private final static Logger log = LoggerFactory.getLogger(HexFileFrame.class);
308
309}