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}