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 javax.swing.JTextArea(); 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 javax.swing.JTextArea 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 workingReply.setSourceLetter("H"); 193 workingReplySet.add(workingReply); // save for later recognition 194 195 CanMessage result = new CanMessage(workingReply.getNumDataElements(), workingReply.getHeader()); 196 for (int i = 0; i < workingReply.getNumDataElements(); i++) { 197 result.setElement(i, workingReply.getElement(i)); 198 } 199 result.setExtended(workingReply.isExtended()); 200 result.setSourceLetter("H"); 201 workingMessageSet.add(result); 202 log.trace("Hub forwarder create reply {}", workingReply); 203 204 // Send over outbound link 205 memo.getTrafficController().sendCanMessage(result, null); // HubPane.this 206 207 // Send into JMRI 208 memo.getTrafficController().distributeOneReply(workingReply, HubPane.this); 209 }); 210 211 t.start(); 212 log.debug("hub thread started"); 213 advertise(port); 214 } 215 216 ArrayList<CanReply> workingReplySet = new ArrayList<>(); // collection of self-sent replies 217 ArrayList<CanMessage> workingMessageSet = new ArrayList<>(); // collection of self-sent messages 218 219 private ZeroConfService _zero_conf_service; 220 protected String zero_conf_addr = "_openlcb-can._tcp.local."; 221 222 protected void advertise(int port) { 223 log.trace("start advertise"); 224 _zero_conf_service = ZeroConfService.create(zero_conf_addr, port); 225 log.trace("start publish"); 226 _zero_conf_service.publish(); 227 log.trace("end publish and advertise"); 228 229 } 230 231 @Override 232 public String getTitle() { 233 if (memo != null) { 234 return Bundle.getMessage("HubControl", memo.getUserName()); // NOI18N 235 } 236 return "LCC / OpenLCB Hub Control"; 237 } 238 239 /** 240 * Creates a Menu List 241 * <p> 242 * Settings : Line Termination 243 */ 244 @Override 245 public List<JMenu> getMenus() { 246 List<JMenu> menuList = new ArrayList<>(); 247 menuList.add(getLineTerminationSettingsMenu()); 248 return menuList; 249 } 250 251 private JMenu getLineTerminationSettingsMenu() { 252 JMenu menu = new JMenu(Bundle.getMessage("LineTermination")); // NOI18N 253 JMenuItem sendLineFeedItem = new JMenuItem(Bundle.getMessage("SendLineTermination")); // NOI18N 254 sendLineFeedItem.addActionListener(this::showSendTerminationDialog); 255 menu.add(sendLineFeedItem); 256 257 JMenuItem requireLineFeedItem = new JMenuItem(Bundle.getMessage("RequireLineTermination")); // NOI18N 258 requireLineFeedItem.addActionListener(this::showRequireTerminationDialog); 259 menu.add(requireLineFeedItem); 260 261 return menu; 262 } 263 264 void showSendTerminationDialog(java.awt.event.ActionEvent e) { 265 JCheckBox checkbox = new JCheckBox(Bundle.getMessage("SendLineTermination")); // NOI18N 266 checkbox.setSelected(_send_line_endings); 267 Object[] params = {Bundle.getMessage("LineTermSettingDialog"), checkbox }; // NOI18N 268 int result = JmriJOptionPane.showConfirmDialog(this, 269 params, 270 Bundle.getMessage("SendLineTermination"), // NOI18N 271 JmriJOptionPane.OK_CANCEL_OPTION); 272 if (result == JmriJOptionPane.OK_OPTION) { 273 _send_line_endings = checkbox.isSelected(); 274 userPreferencesManager.setSimplePreferenceState(getClass().getName() + USER_SAVED, true); // NOI18N 275 userPreferencesManager.setSimplePreferenceState(getClass().getName() + USER_SEND_LINE_ENDINGS, _send_line_endings); // NOI18N 276 } 277 } 278 279 void showRequireTerminationDialog(java.awt.event.ActionEvent e) { 280 JCheckBox checkbox = new JCheckBox(Bundle.getMessage("RequireLineTermination")); // NOI18N 281 checkbox.setSelected(this.getRequireLineEndingsFromUserPref()); 282 Object[] params = {Bundle.getMessage("LineTermSettingDialog"), checkbox }; // NOI18N 283 int result = JmriJOptionPane.showConfirmDialog(this, 284 params, 285 Bundle.getMessage("RequireLineTermination"), // NOI18N 286 JmriJOptionPane.OK_CANCEL_OPTION); 287 if (result == JmriJOptionPane.OK_OPTION) { 288 userPreferencesManager.setSimplePreferenceState(getClass().getName() + USER_REQUIRE_LINE_ENDINGS, checkbox.isSelected()); // NOI18N 289 } 290 } 291 292 @Override 293 public void dispose() { 294 if ( memo != null ) { // set on void initComponents 295 memo.getTrafficController().removeCanListener(this); 296 } 297 if ( _zero_conf_service != null ) { // set on void advertise(int port) 298 _zero_conf_service.stop(); 299 } 300 hub.dispose(); 301 } 302 303 // connection from this JMRI instance - messages received here 304 @Override 305 public synchronized void message(CanMessage l) { // receive a message and log it 306 if ( workingMessageSet.contains(l)) { 307 // ours, don't send 308 workingMessageSet.remove(l); 309 log.debug("suppress forward of message {} from JMRI; WMS={} items", l, workingMessageSet.size()); 310 return; 311 } 312 GridConnectMessage gm = getMessageFrom(l); 313 log.debug("forward message {}",gm); 314 hub.putLine(gm.toString()); 315 } 316 317 /** 318 * Get a GridConnect Message from a CanMessage. 319 * Enables override of the particular type of GridConnectMessage. 320 * @param m the CanMessage 321 * @return a GridConnectMessage. 322 */ 323 protected GridConnectMessage getMessageFrom( CanMessage m ) { 324 return new GridConnectMessage(m); 325 } 326 327 /** 328 * Get an empty GridConnect Reply. 329 * Enables override of the particular type of GridConnectReply. 330 * @return a GridConnectReply. 331 */ 332 protected GridConnectReply getBlankReply( ) { 333 return new GridConnectReply(); 334 } 335 336 // connection from this JMRI instance - replies received here 337 @Override 338 public synchronized void reply(CanReply reply) { 339 if ( workingReplySet.contains(reply)) { 340 // ours, don't send 341 workingReplySet.remove(reply); 342 log.trace("suppress forward of reply {} from JMRI; WRS={} items", reply, workingReplySet.size()); 343 } else { 344 // not ours, forward 345 GridConnectMessage gm = getMessageFrom(new CanMessage(reply)); 346 log.debug("forward reply {} from JMRI, WRS={} items", gm, workingReplySet.size()); 347 hub.putLine(gm.toString()); 348 } 349 } 350 351 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HubPane.class); 352 353}