001package jmri.jmrix.openlcb.swing.memtool;
002
003import java.awt.event.*;
004import java.io.*;
005import java.util.*;
006
007import javax.swing.*;
008import jmri.jmrix.can.CanSystemConnectionMemo;
009import jmri.util.JmriJFrame;
010import jmri.util.swing.WrapLayout;
011import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
012
013import org.openlcb.*;
014import org.openlcb.implementations.*;
015import org.openlcb.swing.*;
016import org.openlcb.swing.memconfig.MemConfigDescriptionPane;
017
018
019/**
020 * Pane for doing various memory operations
021 *
022 * @author Bob Jacobsen Copyright (C) 2023
023 * @since 5.3.4
024 */
025public class MemoryToolPane extends jmri.util.swing.JmriPanel
026        implements jmri.jmrix.can.swing.CanPanelInterface {
027
028    protected CanSystemConnectionMemo memo;
029    Connection connection;
030    NodeID nid;
031
032    MimicNodeStore store;
033    MemoryConfigurationService service;
034    NodeSelector nodeSelector;
035
036    public String getTitle(String menuTitle) {
037        return Bundle.getMessage("TitleMemoryTool");
038    }
039
040    static final int CHUNKSIZE = 64;
041
042    JTextField spaceField;
043    JLabel statusField;
044    JButton gb;
045    JButton pb;
046    JButton cb;
047    boolean cancelled = false;
048    boolean running = false;
049
050    /**
051     * if checked (the default), the Address Space Status
052     * reply will be used to set the length of the read.
053     * The read will also stop on a short-data reply or ann
054     * error reply, including the normal 0x1082 end of data message.
055     * If unchecked, the Address Space Status is skipped
056     * and the read ends on short-data reply or error reply.
057     * <p>
058     * We do not persist this as a preference, because
059     8 we want the default to be trusted and the user to
060     * reselect (or really unselect) as needed.
061     */
062    JCheckBox trustStatusReply;
063
064    @Override
065    public void initComponents(CanSystemConnectionMemo memo) {
066        this.memo = memo;
067        this.connection = memo.get(Connection.class);
068        this.nid = memo.get(NodeID.class);
069
070        store = memo.get(MimicNodeStore.class);
071        EventTable stdEventTable = memo.get(OlcbInterface.class).getEventTable();
072        if (stdEventTable == null) {
073            log.error("no OLCB EventTable found");
074            return;
075        }
076        service = memo.get(MemoryConfigurationService.class);
077
078        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
079
080        // Add to GUI here
081        var ns = new JPanel();
082        ns.setLayout(new WrapLayout());
083        add(ns);
084        nodeSelector = new org.openlcb.swing.NodeSelector(store, Integer.MAX_VALUE);
085        ns.add(nodeSelector);
086        JButton check = new JButton("Check");
087        ns.add(check);
088        check.addActionListener(this::pushedCheckButton);
089
090        var ms = new JPanel();
091        ms.setLayout(new WrapLayout());
092        add(ms);
093        ms.add(new JLabel("Memory Space:"));
094        spaceField = new JTextField("255");
095        ms.add(spaceField);
096
097        trustStatusReply = new JCheckBox("Trust Status Info");
098        trustStatusReply.setSelected(true);
099        ms.add(trustStatusReply);
100
101        var bb = new JPanel();
102        bb.setLayout(new WrapLayout());
103        add(bb);
104        gb = new JButton(Bundle.getMessage("ButtonGet"));
105        bb.add(gb);
106        gb.addActionListener(this::pushedGetButton);
107        pb = new JButton(Bundle.getMessage("ButtonPut"));
108        bb.add(pb);
109        pb.addActionListener(this::pushedPutButton);
110        cb = new JButton(Bundle.getMessage("ButtonCancel"));
111        bb.add(cb);
112        cb.addActionListener(this::pushedCancel);
113
114        bb = new JPanel();
115        bb.setLayout(new WrapLayout());
116        add(bb);
117        statusField = new JLabel("                          ",SwingConstants.CENTER);
118        bb.add(statusField);
119
120        setRunning(false);
121    }
122
123    public MemoryToolPane() {
124    }
125
126    @Override
127    public void dispose() {
128        // and complete this
129        super.dispose();
130    }
131
132    @Override
133    public String getHelpTarget() {
134        return "package.jmri.jmrix.openlcb.swing.memtool.MemoryToolPane";
135    }
136
137    @Override
138    public String getTitle() {
139        if (memo != null) {
140            return (memo.getUserName() + " Memory Tool");
141        }
142        return getTitle(Bundle.getMessage("TitleMemoryTool"));
143    }
144
145    void pushedCheckButton(ActionEvent e) {
146        var node = nodeSelector.getSelectedItem();
147        JmriJFrame f = new JmriJFrame();
148        f.setTitle("Configuration Capabilities");
149
150        var p = new JPanel();
151        f.add(p);
152        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
153
154        JPanel q = new JPanel();
155        q.setLayout(new WrapLayout());
156        p.add(q);
157        q.add(new JLabel(node.toString()));
158
159        p.add(new JSeparator(SwingConstants.HORIZONTAL));
160
161        var nodeMemo = store.findNode(node);
162        String name = "";
163        if (nodeMemo != null) {
164            var ident = nodeMemo.getSimpleNodeIdent();
165                if (ident != null) {
166                    name = ident.getUserName();
167                    q = new JPanel();
168                    q.setLayout(new WrapLayout());
169                    q.add(new JLabel(name));
170                    p.add(q);
171                }
172        }
173
174        MemConfigDescriptionPane mc = new MemConfigDescriptionPane(node, store, service);
175        p.add(mc);
176        mc.initComponents();
177
178        f.pack();
179        f.setVisible(true);
180    }
181
182    void pushedCancel(ActionEvent e) {
183        if (running) {
184            cancelled = true;
185        }
186    }
187
188    void setRunning(boolean t) {
189        if (t) {
190            gb.setEnabled(false);
191            pb.setEnabled(false);
192            cb.setEnabled(true);
193        } else {
194            gb.setEnabled(true);
195            pb.setEnabled(true);
196            cb.setEnabled(false);
197        }
198        running = t;
199    }
200
201    int space = 0xFF;
202
203    NodeID farID = new NodeID("0.0.0.0.0.0");
204
205    MemoryConfigurationService.McsReadHandler cbr =
206        new MemoryConfigurationService.McsReadHandler() {
207            @Override
208            public void handleFailure(int errorCode) {
209                setRunning(false);
210                if (errorCode == 0x1082) {
211                    statusField.setText("Done reading");
212                    log.debug("Stopping read due to 0x1082 status");
213                } if (errorCode == 0x1081) {
214                    log.error("Read failed. Address space not known");
215                    statusField.setText("Read failed. Address space not known");
216                } else {
217                    log.error("Read failed. Error code is {}", String.format("%04X", errorCode));
218                    statusField.setText("Read failed. Error code is "+String.format("%04X", errorCode));
219                }
220                try {
221                    outputStream.flush();
222                    outputStream.close();
223                } catch (IOException ex) {
224                    log.error("Error closing file", ex);
225                    statusField.setText("Error closing output file");
226                }
227            }
228
229            @Override
230            public void handleReadData(NodeID dest, int readSpace, long readAddress, byte[] readData) {
231                log.trace("read succeed with {} bytes at {}", readData.length, readAddress);
232                statusField.setText("Read "+readAddress+" bytes");
233                try {
234                    outputStream.write(readData);
235                } catch (IOException ex) {
236                    log.error("Error writing data to file", ex);
237                    statusField.setText("Error writing data to file");
238                    setRunning(false);
239                    return; // stop now
240                }
241                if (readData.length != CHUNKSIZE) {
242                    // short read is another way to indicate end
243                    statusField.setText("Done reading");
244                    log.debug("Stopping read due to short reply");
245                    setRunning(false);
246                    try {
247                        outputStream.flush();
248                        outputStream.close();
249                    } catch (IOException ex) {
250                        log.error("Error closing file", ex);
251                        statusField.setText("Error closing output file");
252                    }
253                    return;
254                }
255                // fire another unless at endingAddress
256                if (readAddress+readData.length-1 >= endingAddress) { // last address read is length-1 past starting address
257                    // done
258                    setRunning(false);
259                    log.debug("Get operation ending on length");
260                    statusField.setText("Done Reading");
261                }
262                if (!cancelled) {
263                    service.requestRead(farID, space, readAddress+readData.length,
264                                        (int)Math.min(CHUNKSIZE, endingAddress-(readAddress+readData.length-1)),
265                                        cbr);
266                } else {
267                    setRunning(false);
268                    cancelled = false;
269                    log.debug("Get operation cancelled");
270                    statusField.setText("Cancelled");
271                }
272            }
273        };
274
275    OutputStream outputStream;
276    long endingAddress = 0x1000; // token 1MB max if decide not to enquire about it & other methods fail
277
278    /**
279     * Starts reading from node and writing to file process
280     * @param e not used
281     */
282    void pushedGetButton(ActionEvent e) {
283        setRunning(true);
284        farID = nodeSelector.getSelectedItem();
285        try {
286            space = Integer.parseInt(spaceField.getText().trim());
287        } catch (NumberFormatException ex) {
288            log.error("error parsing the space field value \"{}\"", spaceField.getText());
289            statusField.setText("Error parsing the space value");
290            setRunning(false);
291            return;
292        }
293        log.debug("Start get");
294        if (fileChooser == null) {
295            fileChooser = new jmri.util.swing.JmriJFileChooser();
296        }
297        fileChooser.setDialogTitle("Read into binary file");
298        fileChooser.rescanCurrentDirectory();
299        fileChooser.setSelectedFile(new File("memory.bin"));
300
301        int retVal = fileChooser.showSaveDialog(this);
302        if (retVal != JFileChooser.APPROVE_OPTION) {
303            setRunning(false);
304            return;
305        }
306
307        // open file
308        File file = fileChooser.getSelectedFile();
309        log.debug("access {}", file);
310        try {
311            outputStream = new FileOutputStream(file);
312        } catch (IOException ex) {
313            log.error("Error opening file", ex);
314            statusField.setText("Error opening file");
315            setRunning(false);
316            return;
317        }
318
319        if (trustStatusReply.isSelected()) {
320            // request address space info; reply will start read operations.
321            // Memo has to be created here to carry appropriate farID
322            MemoryConfigurationService.McsAddrSpaceMemo cbq =
323                new MemoryConfigurationService.McsAddrSpaceMemo(farID, space) {
324                    @Override
325                    public void handleWriteReply(int errorCode) {
326                        log.error("Get failed with code {}", String.format("%04X", errorCode));
327                        statusField.setText("Get failed with code"+String.format("%04X", errorCode));
328                        setRunning(false);
329                    }
330
331                    @Override
332                    public void handleAddrSpaceData(NodeID dest, int space, long hiAddress, long lowAddress, int flags, String desc) {
333                        // check contents
334                        log.debug("received high Address of {}, low address of {}", hiAddress, lowAddress);
335                        endingAddress = hiAddress;
336                        service.requestRead(farID, space, lowAddress, (int)Math.min(CHUNKSIZE, endingAddress-lowAddress+1), cbr);
337                    }
338                };
339            // start the process by sending the address space request. It's
340            // reply handler will do the first read.
341            service.request(cbq);
342        } else {
343            // kick of read directly, relying on error reply and/or short read for end
344            service.requestRead(farID, space, 0, CHUNKSIZE, cbr);  // assume starting address is zero
345        }
346    }
347
348    MemoryConfigurationService.McsWriteHandler cbw =
349        new MemoryConfigurationService.McsWriteHandler() {
350            @Override
351            public void handleFailure(int errorCode) {
352                if (errorCode == 0x1081) {
353                    log.error("Write failed. Address space not known");
354                    statusField.setText("Write failed. Address space not known.");
355                } else if (errorCode == 0x1083) {
356                    log.error("Write failed. Address space not writable");
357                    statusField.setText("Write failed. Address space not writeable.");
358                } else {
359                    log.error("Write failed. error code is {}", String.format("%04X", errorCode));
360                    statusField.setText("Write failed. error code is "+String.format("%016X", errorCode));
361                }
362                setRunning(false);
363                // return because we're done.
364            }
365
366            @Override
367            public void handleSuccess() {
368                log.trace("Write succeeded {} bytes", address+bytesRead);
369
370                if (cancelled) {
371                    log.debug("Cancelled");
372                    statusField.setText("Cancelled");
373                    setRunning(false);
374                    cancelled = false;
375                }
376                // next operation
377                address = address+bytesRead;
378
379                byte[] dataRead;
380                try {
381                    dataRead = getBytes();
382                    if (dataRead == null) {
383                        // end of read present
384                        setRunning(false);
385                        log.debug("Completed");
386                        statusField.setText("Completed.");
387                        inputStream.close();
388                        return;
389                    }
390                    bytesRead = dataRead.length;
391                    log.trace("write {} bytes", bytesRead);
392                } catch (IOException ex) {
393                    log.error("Error reading file",ex);
394                    return;
395                }
396                service.requestWrite(farID, space, address, dataRead, cbw);
397            }
398        };
399
400    void pushedPutButton(ActionEvent e) {
401        farID = nodeSelector.getSelectedItem();
402        log.debug("Start put");
403        if (fileChooser == null) {
404            fileChooser = new jmri.util.swing.JmriJFileChooser();
405        }
406        fileChooser.setDialogTitle("Upload binary file");
407        fileChooser.rescanCurrentDirectory();
408        fileChooser.setSelectedFile(new File("memory.bin"));
409
410        int retVal = fileChooser.showOpenDialog(this);
411        if (retVal != JFileChooser.APPROVE_OPTION) { return; }
412
413        // open file and read first 64 bytes
414        File file = fileChooser.getSelectedFile();
415        log.debug("access {}", file);
416
417        byte[] dataRead;
418        try {
419            inputStream = new FileInputStream(file);
420            dataRead = getBytes();
421            if (dataRead == null) {
422                // end of read present
423                log.debug("Completed");
424                inputStream.close();
425                return;
426            }
427            bytesRead = dataRead.length;
428            log.trace("read {} bytes", bytesRead);
429        } catch (IOException ex) {
430            log.error("Error reading file",ex);
431            return;
432        }
433
434        // do first memory write
435        address = 0;
436        setRunning(true);
437        service.requestWrite(farID, space, address, dataRead, cbw);
438    }
439
440    byte[] bytes = new byte[CHUNKSIZE];
441    int bytesRead;          // Number bytes read into the bytes[] array from the file. Used for put operation only.
442    InputStream inputStream;
443    int address;
444
445    /**
446     * Read the next bytes, using the 'bytes' member array.
447     *
448     * @return null if has reached end of File
449     * @throws IOException from underlying file access
450     */
451    @SuppressFBWarnings(value="PZLA_PREFER_ZERO_LENGTH_ARRAYS", justification="null indicates end of file")
452    byte[] getBytes() throws IOException {
453        int bytesRead = inputStream.read(bytes); // returned actual number read
454        if (bytesRead == -1) return null;  // file done
455        if (bytesRead == CHUNKSIZE) return bytes;
456        // less data received, have to adjust size of return array
457        return Arrays.copyOf(bytes, bytesRead);
458    }
459
460    // static to remember choice from one use to another.
461    static JFileChooser fileChooser = null;
462
463    /**
464     * Nested class to create one of these using old-style defaults
465     */
466    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
467
468        public Default() {
469            super("Openlcb Memory Tool",
470                    new jmri.util.swing.sdi.JmriJFrameInterface(),
471                    MemoryToolPane.class.getName(),
472                    jmri.InstanceManager.getDefault(jmri.jmrix.can.CanSystemConnectionMemo.class));
473        }
474    }
475
476    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MemoryToolPane.class);
477}