001package jmri.jmrix.openlcb.swing.hub; 002 003import java.awt.BorderLayout; 004 005import java.net.InetAddress; 006import java.nio.charset.StandardCharsets; 007import java.text.DateFormat; 008import java.util.*; 009 010import javax.swing.*; 011 012import jmri.InstanceManager; 013import jmri.UserPreferencesManager; 014 015import jmri.jmrix.can.CanListener; 016import jmri.jmrix.can.CanMessage; 017import jmri.jmrix.can.CanReply; 018import jmri.jmrix.can.CanSystemConnectionMemo; 019import jmri.jmrix.can.adapters.gridconnect.GridConnectMessage; 020import jmri.jmrix.can.adapters.gridconnect.GridConnectReply; 021import jmri.jmrix.can.swing.CanPanelInterface; 022import jmri.util.swing.JmriJOptionPane; 023import jmri.util.zeroconf.ZeroConfService; 024import jmri.util.zeroconf.ZeroConfServiceManager; 025 026import org.openlcb.hub.Hub; 027 028/** 029 * Frame displaying,and more importantly starting, an OpenLCB TCP/IP hub 030 * 031 * @author Bob Jacobsen Copyright (C) 2009, 2010, 2012 032 */ 033public class HubPane extends jmri.util.swing.JmriPanel implements CanListener, CanPanelInterface { 034 035 /** 036 * Create a new HubPane with default options. 037 */ 038 public HubPane() { 039 this(Hub.DEFAULT_PORT); 040 } 041 042 /** 043 * Create a new HubPane with a specified port number. 044 * Sends with Line Endings. 045 * @param port the port number to use. 046 */ 047 public HubPane(int port) { 048 this(port, true); 049 } 050 051 /** 052 * Create a new HubPane with port number and default for sending line ends. 053 * This option may subsequently be ignored by user preference. 054 * Default is to NOT require line endings. 055 * @param port the port number to use. 056 * @param sendLineEndings if no user option is set, true to send line endings, else false. 057 */ 058 public HubPane(int port, boolean sendLineEndings ) { 059 super(); 060 userPreferencesManager = InstanceManager.getDefault(UserPreferencesManager.class); 061 textArea = new java.awt.TextArea(); 062 _send_line_endings = getSendLineEndingsFromUserPref(sendLineEndings); 063 hub = new Hub(port, sendLineEndings, getRequireLineEndingsFromUserPref()) { 064 @Override 065 public void notifyOwner(String line) { 066 SwingUtilities.invokeLater(() -> { 067 textArea.append( 068 System.lineSeparator()+DateFormat.getDateTimeInstance().format(new Date()) + " " + line 069 ); 070 }); 071 } 072 }; 073 } 074 075 private final UserPreferencesManager userPreferencesManager; 076 077 private static final String USER_SAVED = ".UserSaved"; // NOI18N 078 private static final String USER_SEND_LINE_ENDINGS = ".SendLineTermination"; // NOI18N 079 private static final String USER_REQUIRE_LINE_ENDINGS = ".RequireLineTermination"; // NOI18N 080 private boolean _send_line_endings; 081 082 /** 083 * Get the Send line endings setting to use in the Hub. 084 * @param defaultValue normally true for OpenLCB, false for CBUS. 085 * @return the preference, else default value. 086 */ 087 private boolean getSendLineEndingsFromUserPref( boolean defaultValue ){ 088 if ( userPreferencesManager.getSimplePreferenceState(getClass().getName() + USER_SAVED)) { 089 // user has loaded before so use the preference 090 return userPreferencesManager.getSimplePreferenceState(getClass().getName() + USER_SEND_LINE_ENDINGS); 091 } 092 return defaultValue; 093 } 094 095 /** 096 * Get the Require line termination setting to use in the Hub. 097 * @return the preference, default false. 098 */ 099 private boolean getRequireLineEndingsFromUserPref(){ 100 return userPreferencesManager.getSimplePreferenceState(getClass().getName() + USER_REQUIRE_LINE_ENDINGS); 101 } 102 103 CanSystemConnectionMemo memo; 104 105 final transient Hub hub; 106 107 @Override 108 public void initContext(Object context) { 109 log.trace("initContext"); 110 if (context instanceof CanSystemConnectionMemo) { 111 initComponents((CanSystemConnectionMemo) context); 112 } 113 } 114 115 final private java.awt.TextArea textArea; 116 117 @Override 118 public void initComponents(CanSystemConnectionMemo memo) { 119 log.trace("initComponents"); 120 this.memo = memo; 121 122 startHubThread(hub.getPort()); 123 124 // add GUI components 125 setLayout(new BorderLayout()); 126 textArea.setEditable(false); 127 128 add(new JScrollPane(textArea)); 129 add(BorderLayout.CENTER, new JScrollPane(textArea)); 130 131 textArea.append(Bundle.getMessage("HubStarted", // NOI18N 132 DateFormat.getDateTimeInstance().format(new Date()), getTitle())); 133 textArea.append( System.lineSeparator() + Bundle.getMessage("SendLineTermination") // NOI18N 134 +" : "+ _send_line_endings); 135 textArea.append( System.lineSeparator() + Bundle.getMessage("RequireLineTermination") // NOI18N 136 +" : "+ getRequireLineEndingsFromUserPref()); 137 addInetAddresses(); 138 139 // This hears OpenLCB traffic at packet level from traffic controller 140 memo.getTrafficController().addCanListener(this); 141 } 142 143 private void addInetAddresses(){ 144 var t = jmri.util.ThreadingUtil.newThread(() -> { 145 146 log.trace("start addInetAddresses"); 147 ZeroConfServiceManager manager = InstanceManager.getDefault(ZeroConfServiceManager.class); 148 Set<InetAddress> addresses = manager.getAddresses(ZeroConfServiceManager.Protocol.All, true, true); 149 for (InetAddress ha : addresses) { 150 151 var hostAddress = ha.getHostAddress(); 152 var hostName = ha.getHostName(); 153 var hostNameDup = !hostAddress.equals(hostName) ? hostName : ""; 154 var isLoopBack = ha.isLoopbackAddress() ? " Loopback" : ""; // NOI18N 155 var isLinkLocal = ha.isLinkLocalAddress() ? " LinkLocal" : ""; // NOI18N 156 var port = String.valueOf(hub.getPort()); 157 158 jmri.util.ThreadingUtil.runOnGUIEventually( () -> { 159 textArea.append( System.lineSeparator() + Bundle.getMessage(("IpAddressLine"), // NOI18N 160 hostNameDup, isLoopBack, isLinkLocal, hostAddress, port)); 161 log.trace(" added a line"); 162 }); 163 } 164 log.trace("end addInetAddresses"); 165 }, 166 memo.getUserName() + " Hub Thread"); 167 t.start(); 168 } 169 170 Thread t; 171 172 void startHubThread(int port) { 173 t = jmri.util.ThreadingUtil.newThread(hub::start, 174 memo.getUserName() + " Hub Thread"); 175 t.setDaemon(true); 176 177 // add forwarder for internal JMRI traffic 178 hub.addForwarder(m -> { 179 if (m.source == null) { 180 log.trace("not forwarding {} back to JMRI due to null source", m.line); 181 return; // was from this 182 } 183 // process and forward m.line 184 GridConnectReply msg = getBlankReply(); 185 186 byte[] bytes = m.line.getBytes(StandardCharsets.US_ASCII); // GC adapters use ASCII // NOI18N 187 for (int i = 0; i < m.line.length(); i++) { 188 msg.setElement(i, bytes[i]); 189 } 190 191 CanReply workingReply = msg.createReply(); 192 workingReplySet.add(workingReply); // save for later recognition 193 194 CanMessage result = new CanMessage(workingReply.getNumDataElements(), workingReply.getHeader()); 195 for (int i = 0; i < workingReply.getNumDataElements(); i++) { 196 result.setElement(i, workingReply.getElement(i)); 197 } 198 result.setExtended(workingReply.isExtended()); 199 workingMessageSet.add(result); 200 log.trace("Hub forwarder create reply {}", workingReply); 201 202 // Send over outbound link 203 memo.getTrafficController().sendCanMessage(result, null); // HubPane.this 204 205 // Send into JMRI 206 memo.getTrafficController().distributeOneReply(workingReply, HubPane.this); 207 }); 208 209 t.start(); 210 log.debug("hub thread started"); 211 advertise(port); 212 } 213 214 // For testing 215 @SuppressWarnings("deprecation") // Thread.stop 216 void stopHubThread() { 217 if (t != null) { 218 t.stop(); 219 t = null; 220 } 221 } 222 223 ArrayList<CanReply> workingReplySet = new ArrayList<>(); // collection of self-sent replies 224 ArrayList<CanMessage> workingMessageSet = new ArrayList<>(); // collection of self-sent messages 225 226 private ZeroConfService _zero_conf_service; 227 protected String zero_conf_addr = "_openlcb-can._tcp.local."; 228 229 protected void advertise(int port) { 230 log.trace("start advertise"); 231 _zero_conf_service = ZeroConfService.create(zero_conf_addr, port); 232 log.trace("start publish"); 233 _zero_conf_service.publish(); 234 log.trace("end publish and advertise"); 235 236 } 237 238 @Override 239 public String getTitle() { 240 if (memo != null) { 241 return Bundle.getMessage("HubControl", memo.getUserName()); // NOI18N 242 } 243 return "LCC / OpenLCB Hub Control"; 244 } 245 246 /** 247 * Creates a Menu List 248 * <p> 249 * Settings : Line Termination 250 */ 251 @Override 252 public List<JMenu> getMenus() { 253 List<JMenu> menuList = new ArrayList<>(); 254 menuList.add(getLineTerminationSettingsMenu()); 255 return menuList; 256 } 257 258 private JMenu getLineTerminationSettingsMenu() { 259 JMenu menu = new JMenu(Bundle.getMessage("LineTermination")); // NOI18N 260 JMenuItem sendLineFeedItem = new JMenuItem(Bundle.getMessage("SendLineTermination")); // NOI18N 261 sendLineFeedItem.addActionListener(this::showSendTerminationDialog); 262 menu.add(sendLineFeedItem); 263 264 JMenuItem requireLineFeedItem = new JMenuItem(Bundle.getMessage("RequireLineTermination")); // NOI18N 265 requireLineFeedItem.addActionListener(this::showRequireTerminationDialog); 266 menu.add(requireLineFeedItem); 267 268 return menu; 269 } 270 271 void showSendTerminationDialog(java.awt.event.ActionEvent e) { 272 JCheckBox checkbox = new JCheckBox(Bundle.getMessage("SendLineTermination")); // NOI18N 273 checkbox.setSelected(_send_line_endings); 274 Object[] params = {Bundle.getMessage("LineTermSettingDialog"), checkbox }; // NOI18N 275 int result = JmriJOptionPane.showConfirmDialog(this, 276 params, 277 Bundle.getMessage("SendLineTermination"), // NOI18N 278 JmriJOptionPane.OK_CANCEL_OPTION); 279 if (result == JmriJOptionPane.OK_OPTION) { 280 _send_line_endings = checkbox.isSelected(); 281 userPreferencesManager.setSimplePreferenceState(getClass().getName() + USER_SAVED, true); // NOI18N 282 userPreferencesManager.setSimplePreferenceState(getClass().getName() + USER_SEND_LINE_ENDINGS, _send_line_endings); // NOI18N 283 } 284 } 285 286 void showRequireTerminationDialog(java.awt.event.ActionEvent e) { 287 JCheckBox checkbox = new JCheckBox(Bundle.getMessage("RequireLineTermination")); // NOI18N 288 checkbox.setSelected(this.getRequireLineEndingsFromUserPref()); 289 Object[] params = {Bundle.getMessage("LineTermSettingDialog"), checkbox }; // NOI18N 290 int result = JmriJOptionPane.showConfirmDialog(this, 291 params, 292 Bundle.getMessage("RequireLineTermination"), // NOI18N 293 JmriJOptionPane.OK_CANCEL_OPTION); 294 if (result == JmriJOptionPane.OK_OPTION) { 295 userPreferencesManager.setSimplePreferenceState(getClass().getName() + USER_REQUIRE_LINE_ENDINGS, checkbox.isSelected()); // NOI18N 296 } 297 } 298 299 @Override 300 public void dispose() { 301 if ( memo != null ) { // set on void initComponents 302 memo.getTrafficController().removeCanListener(this); 303 } 304 if ( _zero_conf_service != null ) { // set on void advertise(int port) 305 _zero_conf_service.stop(); 306 } 307 stopHubThread(); 308 hub.dispose(); 309 } 310 311 // connection from this JMRI instance - messages received here 312 @Override 313 public synchronized void message(CanMessage l) { // receive a message and log it 314 if ( workingMessageSet.contains(l)) { 315 // ours, don't send 316 workingMessageSet.remove(l); 317 log.debug("suppress forward of message {} from JMRI; WMS={} items", l, workingMessageSet.size()); 318 return; 319 } 320 GridConnectMessage gm = getMessageFrom(l); 321 log.debug("forward message {}",gm); 322 hub.putLine(gm.toString()); 323 } 324 325 /** 326 * Get a GridConnect Message from a CanMessage. 327 * Enables override of the particular type of GridConnectMessage. 328 * @param m the CanMessage 329 * @return a GridConnectMessage. 330 */ 331 protected GridConnectMessage getMessageFrom( CanMessage m ) { 332 return new GridConnectMessage(m); 333 } 334 335 /** 336 * Get an empty GridConnect Reply. 337 * Enables override of the particular type of GridConnectReply. 338 * @return a GridConnectReply. 339 */ 340 protected GridConnectReply getBlankReply( ) { 341 return new GridConnectReply(); 342 } 343 344 // connection from this JMRI instance - replies received here 345 @Override 346 public synchronized void reply(CanReply reply) { 347 if ( workingReplySet.contains(reply)) { 348 // ours, don't send 349 workingReplySet.remove(reply); 350 log.trace("suppress forward of reply {} from JMRI; WRS={} items", reply, workingReplySet.size()); 351 } else { 352 // not ours, forward 353 GridConnectMessage gm = getMessageFrom(new CanMessage(reply)); 354 log.debug("forward reply {} from JMRI, WRS={} items", gm, workingReplySet.size()); 355 hub.putLine(gm.toString()); 356 } 357 } 358 359 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HubPane.class); 360 361}