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}