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}