001package jmri.jmrix.loconet.cmdstnconfig;
002
003import java.awt.FlowLayout;
004import java.awt.GridBagConstraints;
005import java.awt.GridBagLayout;
006import java.awt.event.ActionEvent;
007import java.util.ResourceBundle;
008import javax.swing.BoxLayout;
009import javax.swing.ButtonGroup;
010import javax.swing.JButton;
011import javax.swing.JCheckBox;
012import javax.swing.JLabel;
013import javax.swing.JPanel;
014import javax.swing.JRadioButton;
015import javax.swing.JScrollPane;
016import javax.swing.ScrollPaneConstants;
017import jmri.jmrix.loconet.LnConstants;
018import jmri.jmrix.loconet.LocoNetListener;
019import jmri.jmrix.loconet.LocoNetMessage;
020import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
021import jmri.jmrix.loconet.swing.LnPanel;
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025/**
026 * User interface for Command Station Option Programming.
027 * <p>
028 * Some of the message formats used in this class are Copyright Digitrax, Inc.
029 * and used with permission as part of the JMRI project. That permission does
030 * not extend to uses in other software products. If you wish to use this code,
031 * algorithm or these message formats outside of JMRI, please contact Digitrax
032 * Inc for separate permission.
033 *
034 * @author Alex Shepherd Copyright (C) 2004
035 * @author Bob Jacobsen Copyright (C) 2006
036 */
037public class CmdStnConfigPane extends LnPanel implements LocoNetListener {
038
039    int CONFIG_SLOT = 127;
040    int MIN_OPTION = 1;
041    int MAX_OPTION = 128;
042    int CONFIG_SLOT2 = 126;
043
044    String labelT;
045    String labelC;
046    String labelTop;
047    String read;
048    String write;
049
050    int[] oldcontent = new int[10];
051    int[] oldcontent2 = new int[10];
052
053    JCheckBox optionBox;
054
055    ResourceBundle rb;
056    // internal members to hold widgets
057    JButton readButton;
058    JButton writeButton;
059
060    JRadioButton[] closedButtons = new JRadioButton[MAX_OPTION];
061    JRadioButton[] thrownButtons = new JRadioButton[MAX_OPTION];
062    JLabel[] labels = new JLabel[MAX_OPTION];
063    boolean[] isReserved = new boolean[MAX_OPTION];
064
065    /**
066     * Create a new instance of a Command Station Configuration Pane
067     */
068    public CmdStnConfigPane() {
069        super();
070    }
071
072    @Override
073    public String getHelpTarget() {
074        return "package.jmri.jmrix.loconet.cmdstnconfig.CmdStnConfigFrame"; // NOI18N
075    }
076
077    @Override
078    public String getTitle() {
079        String uName = "";
080        if (memo != null) {
081            uName = memo.getUserName();
082            if (!"LocoNet".equals(uName)) { // NOI18N
083                uName = uName + ": "; // NOI18N
084            } else {
085                uName = "";
086            }
087        }
088        return uName + Bundle.getMessage("MenuItemCmdStnConfig");
089    }
090
091    @Override
092    public void initComponents(LocoNetSystemConnectionMemo memo) {
093        super.initComponents(memo);
094
095        // set up constants from properties file, if possible
096        String name = "<unchanged>"; // NOI18N
097        try {
098            name = memo.getSlotManager().getCommandStationType().getName();
099            // get first token
100            if (name.indexOf(' ') != -1) {
101                name = name.substring(0, name.indexOf(' '));
102            }
103            name = name.replace("+", "Plus");
104            log.debug("match /{}/", name); // NOI18N
105            rb = ResourceBundle.getBundle("jmri.jmrix.loconet.cmdstnconfig." + name + "options"); // NOI18N
106        } catch (Exception e) { // use standard option set
107            log.warn("Failed to find properties for /{}/ command station type", name, e); // NOI18N
108            rb = ResourceBundle.getBundle("jmri.jmrix.loconet.cmdstnconfig.Defaultoptions"); // NOI18N
109            // Localized strings common to all LocoNet command station models
110            // are fetched using Bundle.getMessage()
111        }
112
113        try {
114            CONFIG_SLOT = Integer.parseInt(rb.getString("CONFIG_SLOT"));
115            MIN_OPTION = Integer.parseInt(rb.getString("MIN_OPTION"));
116            MAX_OPTION = Integer.parseInt(rb.getString("MAX_OPTION"));
117            if (MAX_OPTION > 64) {
118                CONFIG_SLOT2 = Integer.parseInt(rb.getString("CONFIG_SLOT2"));
119            }
120        } catch (NumberFormatException e) {
121            log.error("Failed to load values from /{}/ properties", name); // NOI18N
122        }
123        log.debug("Constants: {} {} {}", CONFIG_SLOT, MIN_OPTION, MAX_OPTION); // NOI18N
124
125        labelT = Bundle.getMessage("StateThrownShort");
126        labelC = Bundle.getMessage("StateClosedShort");
127        labelTop = rb.getString("LabelTop");
128        read = Bundle.getMessage("ButtonRead");
129        write = Bundle.getMessage("ButtonWrite");
130        String tooltip = Bundle.getMessage("CmdStnConfigFxToolTip");
131
132        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
133
134        {
135            // start with the CS title
136            add(new JLabel(labelTop));
137
138            // section holding buttons
139            readButton = new JButton(read);
140            writeButton = new JButton(write);
141
142            JPanel pane = new JPanel();
143            pane.setLayout(new FlowLayout());
144            pane.add(readButton);
145            pane.add(writeButton);
146            if (CONFIG_SLOT == -1) { // disable reading/writing for
147                                     // non-configurable CS types, ie.
148                                     // Intellibox-I/-II
149                readButton.setEnabled(false);
150                writeButton.setEnabled(false);
151            }
152            add(pane);
153
154            optionBox = new JCheckBox(Bundle.getMessage("CheckBoxReserved"));
155            add(optionBox);
156
157            // heading
158            add(new JLabel(Bundle.getMessage("HeadingText")));
159
160            // section holding options
161            JPanel options = new JPanel();
162            GridBagConstraints gc = new GridBagConstraints();
163            GridBagLayout gl = new GridBagLayout();
164            gc.gridy = 0;
165            gc.ipady = 0;
166
167            options.setLayout(gl);
168            for (int i = MIN_OPTION; i <= MAX_OPTION; i++) {
169                JPanel p2 = new JPanel();
170                p2.setLayout(new FlowLayout());
171                ButtonGroup g = new ButtonGroup();
172                JRadioButton c = new JRadioButton(labelC);
173                JRadioButton t = new JRadioButton(labelT);
174                g.add(c);
175                g.add(t);
176
177                p2.add(t);
178                p2.add(c);
179                closedButtons[i - MIN_OPTION] = c;
180                thrownButtons[i - MIN_OPTION] = t;
181                gc.weightx = 1.0;
182                gc.gridx = 0;
183                gc.anchor = GridBagConstraints.CENTER;
184                gl.setConstraints(p2, gc);
185                options.add(p2);
186                gc.gridx = 1;
187                gc.weightx = GridBagConstraints.REMAINDER;
188                gc.anchor = GridBagConstraints.WEST;
189                String label;
190                try {
191                    label = rb.getString("Option" + i); // model specific Option
192                                                        // descriptions NOI18N
193                    isReserved[i - MIN_OPTION] = false;
194                } catch (java.util.MissingResourceException e) {
195                    label = "" + i + ": " + Bundle.getMessage("Reserved");
196                    isReserved[i - MIN_OPTION] = true;
197                }
198                JLabel l = new JLabel(label);
199                if (i > 20 && i < 24) {
200                    log.debug("CS name: {}", name);
201                    if (name.startsWith("DB150")) {
202                        // DB150 is the only model using different OpSw 21-23
203                        // combos than the common tooltip, which is stored in
204                        // LocoNetBundle
205                        tooltip = rb.getString("DB150ConfigFxToolTip");
206                    } else if  (name.startsWith("DCS52")) {
207                        tooltip = rb.getString("DCS52ConfigFxToolTip");
208                    } else if  (name.startsWith("DCS240Plus")) {
209                        tooltip = rb.getString("DCS240PlusConfigFxToolTip");
210                    } else if  (name.startsWith("DCS240")) {
211                        tooltip = rb.getString("DCS240ConfigFxToolTip");
212                    } else if  (name.startsWith("DCS210Plus")) {
213                        tooltip = rb.getString("DCS210PlusConfigFxToolTip");
214                    } else if  (name.startsWith("DCS210")) {
215                        tooltip = rb.getString("DCS210ConfigFxToolTip");
216                    }
217                    t.setToolTipText(tooltip);
218                    c.setToolTipText(tooltip);
219                    l.setToolTipText(tooltip);
220                }
221                labels[i - MIN_OPTION] = l;
222                gl.setConstraints(l, gc);
223                options.add(l);
224                gc.gridy++;
225            }
226            JScrollPane js = new JScrollPane(options);
227            js.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
228            js.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
229            add(js);
230
231        }
232
233        optionBox.addActionListener((ActionEvent e) -> {
234            updateVisibility(optionBox.isSelected());
235        });
236        readButton.addActionListener((ActionEvent e) -> {
237            readButtonActionPerformed(e);
238        });
239        writeButton.addActionListener((ActionEvent e) -> {
240            writeButtonActionPerformed(e);
241        });
242
243        updateVisibility(optionBox.isSelected());
244
245        // connect to the LnTrafficController
246        memo.getLnTrafficController().addLocoNetListener(~0, this);
247
248        // and start
249        start();
250    }
251
252    void updateVisibility(boolean show) {
253        for (int i = MIN_OPTION; i <= MAX_OPTION; i++) {
254            if (isReserved[i - MIN_OPTION]) {
255                closedButtons[i - MIN_OPTION].setVisible(show);
256                thrownButtons[i - MIN_OPTION].setVisible(show);
257                labels[i - MIN_OPTION].setVisible(show);
258            }
259        }
260        revalidate();
261    }
262
263    public void readButtonActionPerformed(java.awt.event.ActionEvent e) {
264        // format and send request
265        start();
266    }
267
268    public void writeButtonActionPerformed(java.awt.event.ActionEvent e) {
269
270        updateSlot(CONFIG_SLOT, MIN_OPTION, MAX_OPTION <= 64 ? MAX_OPTION : 64, oldcontent);
271        if (MAX_OPTION > 64) {
272            updateSlot(CONFIG_SLOT2, 65, MAX_OPTION, oldcontent2);
273        }
274    }
275
276    public void updateSlot(int opSwSlot, int firstOpSw, int lastOpsw, int[] oldData) {
277        LocoNetMessage msg = new LocoNetMessage(14);
278        msg.setElement(0, LnConstants.OPC_WR_SL_DATA);
279        msg.setElement(1, 0x0E);
280        msg.setElement(2, opSwSlot);
281
282        // load last seen contents into message
283        for (int i = 0; i < 10; i++) {
284            msg.setElement(3 + i, oldData[i]);
285        }
286
287        int byteBase = (firstOpSw / 64) * 64;
288        // button 0 = opsw 1
289        for (int i = firstOpSw - 1; i <= lastOpsw - 1; i++) {
290            // i indexes over closed buttons - 1
291            int byteIndex = (i - byteBase) / 8; // byteIndex = 0 is the first
292                                                // payload byte
293            if (byteIndex > 3) {
294                byteIndex++; // Skip the 4th payload byte for some reason
295            }
296            byteIndex += 3; // Add base offset into slot message to first data
297                            // byte
298
299            int bitIndex = i % 8;
300            int bitMask = 0x01 << bitIndex;
301
302            if (closedButtons[i].isSelected()) {
303                msg.setElement(byteIndex, msg.getElement(byteIndex) | bitMask);
304            } else {
305                msg.setElement(byteIndex, msg.getElement(byteIndex) & ~bitMask);
306            }
307        }
308
309        // send message
310        memo.getLnTrafficController().sendLocoNetMessage(msg);
311    }
312
313    /**
314     * Start the Frame operating by asking for a read.
315     */
316    public void start() {
317        // format and send request for slot contents
318        LocoNetMessage l = new LocoNetMessage(4);
319        l.setElement(0, LnConstants.OPC_RQ_SL_DATA);
320        l.setElement(1, CONFIG_SLOT);
321        l.setElement(2, 0);
322        l.setElement(3, 0);
323        memo.getLnTrafficController().sendLocoNetMessage(l);
324        if (MAX_OPTION > 64) {
325            // need second slot
326            l.setElement(1, CONFIG_SLOT2);
327            memo.getLnTrafficController().sendLocoNetMessage(l);
328        }
329    }
330
331    /**
332     * Process the incoming message to look for Slot 127 Read.
333     */
334    @Override
335    public void message(LocoNetMessage msg) {
336        if (msg.getOpCode() != LnConstants.OPC_SL_RD_DATA) {
337            return;
338        }
339        if (msg.getElement(2) == CONFIG_SLOT) {
340            // save contents for later
341            for (int i = 0; i < 10; i++) {
342                oldcontent[i] = msg.getElement(3 + i);
343            }
344
345            // set the GUI
346            int iLimit = MAX_OPTION <= 63 ? MAX_OPTION - 1 : 63;
347            for (int i = 0; i <= iLimit; i++) {
348                // i indexes over closed/thrown buttons
349                int byteIndex = i / 8; // index = 0 is the first payload byte
350                if (byteIndex > 3) {
351                    byteIndex++; // Skip the 4th payload byte for some reason
352                }
353                byteIndex += 3; // Add base offset to first data byte
354
355                int bitIndex = i % 8;
356                int bitMask = 0x01 << bitIndex;
357
358                int data = msg.getElement(byteIndex); // data is the payload
359                                                      // byte
360                if ((data & bitMask) != 0) {
361                    closedButtons[i].setSelected(true);
362                } else {
363                    thrownButtons[i].setSelected(true);
364                }
365            }
366        } else if (msg.getElement(2) == CONFIG_SLOT2 && MAX_OPTION > 64) {
367
368            // save contents for later
369            for (int i = 0; i < 10; i++) {
370                oldcontent2[i] = msg.getElement(3 + i);
371            }
372
373            // set the GUI for option sw 64 thru MAX
374            // note indexes are 0 based sor start at 63
375
376            for (int i = 63; i <= MAX_OPTION - 1; i++) {
377                // i indexes over closed/thrown buttons
378                int byteIndex = (i - 63) / 8; // index = 0 is the first payload
379                                              // byte
380                if (byteIndex > 3) {
381                    byteIndex++; // Skip the 4th payload byte for some reason
382                }
383                byteIndex += 3; // Add base offset to first data byte
384
385                int bitIndex = i % 8;
386                int bitMask = 0x01 << bitIndex;
387
388                int data = msg.getElement(byteIndex); // data is the payload
389                                                      // byte
390                if ((data & bitMask) != 0) {
391                    closedButtons[i].setSelected(true);
392                } else {
393                    thrownButtons[i].setSelected(true);
394                }
395            }
396        } else {
397            // do nothing
398        }
399        log.debug("Config Slot Data: {}", msg);
400    }
401
402    @Override
403    public void dispose() {
404        // disconnect from LnTrafficController
405        memo.getLnTrafficController().removeLocoNetListener(~0, this);
406        super.dispose();
407    }
408
409    // initialize logging
410    private final static Logger log = LoggerFactory.getLogger(CmdStnConfigPane.class);
411
412}