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