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}