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