001package jmri.jmrix.dccpp.swing.virtuallcd; 002 003import java.awt.*; 004import java.beans.PropertyChangeListener; 005import java.io.*; 006import java.util.List; 007import java.util.*; 008 009import javax.annotation.CheckForNull; 010import javax.swing.*; 011 012import jmri.jmrit.display.Positionable; 013import jmri.jmrix.ConnectionStatus; 014import jmri.jmrix.dccpp.*; 015import jmri.util.JmriJFrame; 016 017/** 018 * Panel to image the DCC-EX command station's OLED display. 019 * Also sends request to DCC-EX to send copies of all LCD messages to this 020 * instance of JMRI. 021 * 022 * @author Bob Jacobsen Copyright (C) 2023 023 * @author M Steve Todd Copyright (C) 2023 024 * @author Daniel Bergqvist Copyright (C) 2026 025 */ 026public class VirtualLCDPanel extends JPanel 027 implements DCCppListener, VirtualLCDConfiguration { 028 029 private final boolean _isMemoEditable; 030 private final JmriJFrame _frame; 031 private final Positionable _positionable; 032 private DCCppTrafficController _tc; 033 private final PropertyChangeListener _listener; 034 private Font font; 035 private DCCppSystemConnectionMemo _memo; 036 private DisplayConfig _displayConfig = DisplayConfig.ConfigureVirtualLCD_AllDisplays; 037 private int displayNo = -1; 038 private int _minDisplayNo; 039 private int _maxDisplayNo; 040 private final Set<Integer> _selectedDisplays = new HashSet<>(); 041 private Dimension lcdSize; 042 043 static final int TOTALLINES = 64; 044 private final Map<Integer, List<JLabel>> linesMap = new HashMap<>(); 045 private final JLabel noVisibleDisplays = new JLabel(Bundle.getMessage("VirtualLcdNoVisibleDisplays")); 046 047 public VirtualLCDPanel(JmriJFrame frame, boolean isMemoEditable) { 048 this(frame, null, isMemoEditable); 049 } 050 051 public VirtualLCDPanel(JmriJFrame frame, Positionable pos, boolean isMemoEditable) { 052 053 _isMemoEditable = isMemoEditable; 054 _frame = frame; 055 _positionable = pos; 056 057 _listener = evt -> { 058 if (ConnectionStatus.CONNECTION_UP.equals(ConnectionStatus.instance().getConnectionState(_memo))) { 059 _tc.sendDCCppMessage(DCCppMessage.makeLCDRequestMsg(), null); 060 } 061 }; 062 } 063 064 public void initComponents() { 065 _tc = _memo.getDCCppTrafficController(); 066 _tc.addDCCppListener(DCCppInterface.CS_INFO, this); 067 068 _tc.sendDCCppMessage(DCCppMessage.makeLCDRequestMsg(), null); 069 ConnectionStatus.instance().addPropertyChangeListener(_memo, _listener); 070 071 setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); 072 073 // load the custom 5x8 found 074 try { 075 InputStream stream = new FileInputStream(new File("resources/fonts/5x8_lcd_hd44780u_a02.ttf")); 076 font = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(16f).deriveFont(Font.BOLD); 077 } catch (IOException e1) { log.error("failed to find or open font file"); 078 } catch (FontFormatException e2) { log.error("font file not valid"); 079 } 080 081 noVisibleDisplays.setOpaque(true); 082 noVisibleDisplays.setBackground(Color.BLACK); 083 noVisibleDisplays.setForeground(new Color(255,63,63)); // Red 084 if (font != null) noVisibleDisplays.setFont(font); 085 add(noVisibleDisplays); 086 packOrResize(); 087 } 088 089 public void reset() { 090 this.removeAll(); 091 add(noVisibleDisplays); 092 packOrResize(); 093 linesMap.clear(); 094 } 095 096 public void dispose() { 097 ConnectionStatus.instance().removePropertyChangeListener(_memo, _listener); 098 } 099 100 @Override 101 public void setMemo(DCCppSystemConnectionMemo memo) { 102 ConnectionStatus.instance().removePropertyChangeListener(_memo, _listener); 103 if (_tc != null) { 104 _tc.removeDCCppListener(DCCppInterface.CS_INFO, this); 105 } 106 _memo = memo; 107 _tc = memo.getDCCppTrafficController(); 108 _tc.addDCCppListener(DCCppInterface.CS_INFO, this); 109 _tc.sendDCCppMessage(DCCppMessage.makeLCDRequestMsg(), null); 110 ConnectionStatus.instance().addPropertyChangeListener(_memo, _listener); 111 reset(); 112 } 113 114 @Override 115 public DCCppSystemConnectionMemo getMemo() { 116 return _memo; 117 } 118 119 @Override 120 public boolean isMemoEditable() { 121 return _isMemoEditable; 122 } 123 124 @Override 125 public void setDisplayConfig(DisplayConfig displayConfig) { 126 this._displayConfig = displayConfig; 127 reset(); 128 } 129 130 @Override 131 public DisplayConfig getDisplayConfig() { 132 return _displayConfig; 133 } 134 135 @Override 136 public void setDisplayNo(int displayNo) { 137 this.displayNo = displayNo; 138 reset(); 139 } 140 141 @Override 142 public int getDisplayNo() { 143 return displayNo; 144 } 145 146 @Override 147 public void setMinDisplayNo(int minDisplayNo) { 148 this._minDisplayNo = minDisplayNo; 149 reset(); 150 } 151 152 @Override 153 public int getMinDisplayNo() { 154 return _minDisplayNo; 155 } 156 157 @Override 158 public void setMaxDisplayNo(int maxDisplayNo) { 159 this._maxDisplayNo = maxDisplayNo; 160 reset(); 161 } 162 163 @Override 164 public int getMaxDisplayNo() { 165 return _maxDisplayNo; 166 } 167 168 @Override 169 public void setSelectedDisplays(Set<Integer> displays) { 170 _selectedDisplays.clear(); 171 _selectedDisplays.addAll(displays); 172 reset(); 173 } 174 175 @Override 176 public Set<Integer> getSelectedDisplays() { 177 return _selectedDisplays; 178 } 179 180 @Override 181 public void setLCDSize(@CheckForNull Dimension d) { 182 if (d != null && d.width > 0 && d.height > 0) { 183 lcdSize = d; 184 } else { 185 lcdSize = null; 186 } 187 reset(); 188 } 189 190 @CheckForNull 191 @Override 192 public Dimension getLCDSize() { 193 return lcdSize; 194 } 195 196 public String getNameString() { 197 switch (_displayConfig) { 198 case ConfigureVirtualLCD_AllDisplays: 199 return Bundle.getMessage("VirtualLcdPositionable_AllDisplays"); 200 201 case ConfigureVirtualLCD_OneDisplay: 202 return Bundle.getMessage( 203 "VirtualLcdPositionable_OneDisplay", displayNo); 204 205 case ConfigureVirtualLCD_IntervalDisplay: 206 return Bundle.getMessage( 207 "VirtualLcdPositionable_IntervalDisplay", 208 _minDisplayNo, _maxDisplayNo); 209 210 case ConfigureVirtualLCD_SelectedDisplays: 211 StringBuilder sb = new StringBuilder(); 212 for (int i : _selectedDisplays) { 213 if (sb.length() > 0) { 214 sb.append(","); 215 } 216 sb.append(i); 217 } 218 return Bundle.getMessage( 219 "VirtualLcdPositionable_SelectedDisplays", sb.toString()); 220 221 default: 222 throw new IllegalArgumentException("Unknown displayConfig: "+_displayConfig.name()); 223 } 224 } 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override 230 public void message(DCCppMessage msg) { 231 } 232 233 private List<JLabel> createNewDisplay() { 234 // noVisibleDisplays is shown until at least one display is added. 235 this.remove(noVisibleDisplays); 236 237 // Add space between displays if this is not the first display 238 if (getComponentCount() > 0) { 239 Component c = Box.createHorizontalStrut(10); 240 this.add(c); 241 } 242 243 var lines = new ArrayList<JLabel>(TOTALLINES + 1); 244 var pane = new JPanel(); 245 pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS)); 246 // initialize the list of display lines 247 for (int i = 0; i<TOTALLINES; i++) { 248 var label = new JLabel(); 249 if (lcdSize != null && lines.size() < lcdSize.height) { 250 label.setText(cutIfNeeded("")); 251 } 252 if (font != null) label.setFont(font); 253 label.setOpaque(true); 254 label.setBackground(Color.BLACK); 255 label.setForeground(Color.WHITE); 256 lines.add(label); 257 pane.add(lines.get(i)); 258 } 259 pane.setOpaque(true); 260 pane.setBackground(Color.BLACK); 261 pane.setAlignmentY(Component.TOP_ALIGNMENT); 262 this.add(pane); 263 packOrResize(); 264 return lines; 265 } 266 267 private boolean showDisplay(int displayNumber) { 268 switch (_displayConfig) { 269 case ConfigureVirtualLCD_AllDisplays: 270 return true; 271 272 case ConfigureVirtualLCD_OneDisplay: 273 return displayNo == displayNumber; 274 275 case ConfigureVirtualLCD_IntervalDisplay: 276 return _minDisplayNo <= displayNumber && displayNumber <= _maxDisplayNo; 277 278 case ConfigureVirtualLCD_SelectedDisplays: 279 return _selectedDisplays.contains(displayNumber); 280 281 default: 282 throw new IllegalArgumentException("Unknown displayConfig: "+_displayConfig.name()); 283 } 284 } 285 286 private String cutIfNeeded(String s) { 287 if (lcdSize != null) { 288 while (s.length() < lcdSize.width) { 289 s += " "; 290 } 291 if (s.length() > lcdSize.width) { 292 s = s.substring(0, lcdSize.width); 293 } 294 } 295 return s; 296 } 297 298 private void packOrResize() { 299 if (_positionable != null) { 300 var d = this.getPreferredSize(); 301 this.setSize(d); 302 _positionable.setSize(d); 303 } else { 304 _frame.pack(); 305 } 306 } 307 308 /** 309 * {@inheritDoc} 310 */ 311 @Override 312 public void message(DCCppReply msg) { 313 if (msg.isLCDTextReply()) { // <@ display# line# "message text"> 314 int displayNumber = msg.getLCDDisplayNumInt(); 315 316 if (showDisplay(displayNumber)) { 317 var lines = linesMap.computeIfAbsent(displayNumber, display -> createNewDisplay()); 318 int lineNumber = msg.getLCDLineNumInt(); 319 if (lcdSize == null || lineNumber < lcdSize.height) { 320 if (lineNumber < TOTALLINES) { 321 lines.get(lineNumber).setText(cutIfNeeded(msg.getLCDTextString()+" ")); // padding for appearance 322 packOrResize(); 323 } else { 324 log.warn("Received LCD message for line {}, but configured for TOTALLINES limit of {}", 325 lineNumber, TOTALLINES-1); 326 } 327 } 328 log.debug("Received LCD message for display# {}.", displayNumber); 329 } 330 } 331 } 332 333 /** 334 * {@inheritDoc} 335 */ 336 @Override 337 public void notifyTimeout(DCCppMessage msg) { 338 } 339 340 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VirtualLCDPanel.class); 341 342}