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}