altosui: Finish radio scanning UI
[fw/altos] / altosui / AltosScanUI.java
1 /*
2  * Copyright © 2011 Keith Packard <keithp@keithp.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package altosui;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import javax.swing.*;
23 import javax.swing.filechooser.FileNameExtensionFilter;
24 import javax.swing.table.*;
25 import javax.swing.event.*;
26 import java.io.*;
27 import java.util.*;
28 import java.text.*;
29 import java.util.prefs.*;
30 import java.util.concurrent.*;
31
32 class AltosScanResult {
33         String  callsign;
34         int     serial;
35         int     flight;
36         int     channel;
37         int     telemetry;
38         static final String[] short_monitor_names = { "Standard", "Original" };
39         
40         boolean interrupted = false;
41         
42         public String toString() {
43                 return String.format("%-9.9s serial %-4d flight %-4d (channel %-2d telemetry %s)",
44                                      callsign, serial, flight, channel, short_monitor_names[telemetry]);
45         }
46
47         public String toShortString() {
48                 return String.format("%s %d %d %d %d",
49                                      callsign, serial, flight, channel, telemetry);
50         }
51
52         public AltosScanResult(String in_callsign, int in_serial,
53                                int in_flight, int in_channel, int in_telemetry) {
54                 callsign = in_callsign;
55                 serial = in_serial;
56                 flight = in_flight;
57                 channel = in_channel;
58                 telemetry = in_telemetry;
59         }
60
61         public boolean equals(AltosScanResult other) {
62                 return (callsign.equals(other.callsign) &&
63                         serial == other.serial &&
64                         flight == other.flight &&
65                         channel == other.channel &&
66                         telemetry == other.telemetry);
67         }
68 }
69
70 class AltosScanResults extends LinkedList<AltosScanResult> implements ListModel {
71         
72         LinkedList<ListDataListener>    listeners = new LinkedList<ListDataListener>();
73
74         public boolean add(AltosScanResult r) {
75                 for (AltosScanResult old : this)
76                         if (old.equals(r))
77                                 return true;
78
79                 super.add(r);
80                 ListDataEvent   de = new ListDataEvent(this,
81                                                        ListDataEvent.INTERVAL_ADDED,
82                                                        this.size() - 2, this.size() - 1);
83                 for (ListDataListener l : listeners)
84                         l.contentsChanged(de);
85                 return true;
86         }
87
88         public void addListDataListener(ListDataListener l) {
89                 listeners.add(l);
90         }
91         
92         public void removeListDataListener(ListDataListener l) {
93                 listeners.remove(l);
94         }
95
96         public AltosScanResult getElementAt(int i) {
97                 return this.get(i);
98         }
99
100         public int getSize() {
101                 return this.size();
102         }
103 }
104
105 public class AltosScanUI
106         extends JDialog
107         implements ActionListener
108 {
109         AltosUI                         owner;
110         AltosDevice                     device;
111         AltosTelemetryReader            reader;
112         private JList                   list;
113         private JLabel                  scanning_label;
114         private JButton                 cancel_button;
115         private JButton                 monitor_button;
116         javax.swing.Timer               timer;
117         AltosScanResults                results = new AltosScanResults();
118
119         static final String[]           monitor_names = { "Standard AltOS Telemetry", "Original TeleMetrum Telemetry" };
120         static final int[]              monitors = { 2, 1 };
121         int                             monitor;
122         int                             channel;
123
124         final static int                timeout = 1200;
125         TelemetryHandler                handler;
126         Thread                          thread;
127
128         void scan_exception(Exception e) {
129                 if (e instanceof FileNotFoundException) {
130                         JOptionPane.showMessageDialog(owner,
131                                                       String.format("Cannot open device \"%s\"",
132                                                                     device.toShortString()),
133                                                       "Cannot open target device",
134                                                       JOptionPane.ERROR_MESSAGE);
135                 } else if (e instanceof AltosSerialInUseException) {
136                         JOptionPane.showMessageDialog(owner,
137                                                       String.format("Device \"%s\" already in use",
138                                                                     device.toShortString()),
139                                                       "Device in use",
140                                                       JOptionPane.ERROR_MESSAGE);
141                 } else if (e instanceof IOException) {
142                         IOException ee = (IOException) e;
143                         JOptionPane.showMessageDialog(owner,
144                                                       device.toShortString(),
145                                                       ee.getLocalizedMessage(),
146                                                       JOptionPane.ERROR_MESSAGE);
147                 } else {
148                         JOptionPane.showMessageDialog(owner,
149                                                       String.format("Connection to \"%s\" failed",
150                                                                     device.toShortString()),
151                                                       "Connection Failed",
152                                                       JOptionPane.ERROR_MESSAGE);
153                 }
154                 close();
155         }
156
157         class TelemetryHandler implements Runnable {
158
159                 public void run() {
160
161                         boolean interrupted = false;
162
163                         try {
164                                 for (;;) {
165                                         try {
166                                                 AltosRecord     record = reader.read();
167                                                 if (record == null)
168                                                         continue;
169                                                 if ((record.seen & AltosRecord.seen_flight) != 0) {
170                                                         final AltosScanResult   result = new AltosScanResult(record.callsign,
171                                                                                                      record.serial,
172                                                                                                      record.flight,
173                                                                                                      channel,
174                                                                                                      monitor);
175                                                         Runnable r = new Runnable() {
176                                                                         public void run() {
177                                                                                 results.add(result);
178                                                                         }
179                                                                 };
180                                                         SwingUtilities.invokeLater(r);
181                                                 }
182                                         } catch (ParseException pp) {
183                                         } catch (AltosCRCException ce) {
184                                         }
185                                 }
186                         } catch (InterruptedException ee) {
187                                 interrupted = true;
188                         } catch (IOException ie) {
189                         } finally {
190                                 reader.close(interrupted);
191                         }
192                 }
193         }
194
195         void set_label() {
196                 scanning_label.setText(String.format("Scanning: channel %d %s",
197                                                      channel,
198                                                      monitor_names[monitor]));
199         }
200
201         void next() {
202                 reader.serial.set_monitor(false);
203                 try {
204                         Thread.sleep(100);
205                 } catch (InterruptedException ie){
206                 }
207                 ++channel;
208                 if (channel > 9) {
209                         channel = 0;
210                         ++monitor;
211                         if (monitor == monitors.length)
212                                 monitor = 0;
213                         reader.serial.set_telemetry(monitors[monitor]);
214                 }
215                 reader.serial.set_channel(channel);
216                 set_label();
217                 reader.serial.set_monitor(true);
218         }
219
220
221         void close() {
222                 if (thread != null && thread.isAlive()) {
223                         thread.interrupt();
224                         try {
225                                 thread.join();
226                         } catch (InterruptedException ie) {}
227                 }
228                 thread = null;
229                 if (timer != null)
230                         timer.stop();
231                 setVisible(false);
232                 dispose();
233         }
234
235         void tick_timer() {
236                 next();
237         }
238
239         public void actionPerformed(ActionEvent e) {
240                 String cmd = e.getActionCommand();
241
242                 if (cmd.equals("cancel"))
243                         close();
244
245                 if (cmd.equals("tick"))
246                         tick_timer();
247
248                 if (cmd.equals("monitor")) {
249                         close();
250                         AltosScanResult r = (AltosScanResult) (list.getSelectedValue());
251                         if (r != null) {
252                                 if (device != null) {
253                                         if (reader != null) {
254                                                 reader.set_telemetry(monitors[r.telemetry]);
255                                                 reader.set_channel(r.channel);
256                                                 owner.telemetry_window(device);
257                                         }
258                                 }
259                         }
260                 }
261         }
262
263         /* A window listener to catch closing events and tell the config code */
264         class ConfigListener extends WindowAdapter {
265                 AltosScanUI     ui;
266
267                 public ConfigListener(AltosScanUI this_ui) {
268                         ui = this_ui;
269                 }
270
271                 public void windowClosing(WindowEvent e) {
272                         ui.actionPerformed(new ActionEvent(e.getSource(),
273                                                            ActionEvent.ACTION_PERFORMED,
274                                                            "close"));
275                 }
276         }
277
278         private boolean open() {
279                 device = AltosDeviceDialog.show(owner, Altos.product_basestation);
280                 if (device == null)
281                         return false;
282                 try {
283                         reader = new AltosTelemetryReader(device);
284                         reader.serial.set_channel(channel);
285                         reader.serial.set_telemetry(monitors[monitor]);
286                         handler = new TelemetryHandler();
287                         thread = new Thread(handler);
288                         thread.start();
289                         return true;
290                 } catch (FileNotFoundException ee) {
291                         JOptionPane.showMessageDialog(owner,
292                                                       String.format("Cannot open device \"%s\"",
293                                                                     device.toShortString()),
294                                                       "Cannot open target device",
295                                                       JOptionPane.ERROR_MESSAGE);
296                 } catch (AltosSerialInUseException si) {
297                         JOptionPane.showMessageDialog(owner,
298                                                       String.format("Device \"%s\" already in use",
299                                                                     device.toShortString()),
300                                                       "Device in use",
301                                                       JOptionPane.ERROR_MESSAGE);
302                 } catch (IOException ee) {
303                         JOptionPane.showMessageDialog(owner,
304                                                       device.toShortString(),
305                                                       "Unkonwn I/O error",
306                                                       JOptionPane.ERROR_MESSAGE);
307                 }
308                 if (reader != null)
309                         reader.close(false);
310                 return false;
311         }
312
313         public AltosScanUI(AltosUI in_owner) {
314
315                 owner = in_owner;
316
317                 if (!open())
318                         return;
319
320                 Container               pane = getContentPane();
321                 GridBagConstraints      c = new GridBagConstraints();
322                 Insets                  i = new Insets(4,4,4,4);
323
324                 timer = new javax.swing.Timer(timeout, this);
325                 timer.setActionCommand("tick");
326                 timer.restart();
327
328                 owner = in_owner;
329
330                 pane.setLayout(new GridBagLayout());
331
332                 scanning_label = new JLabel("Scanning:");
333                 
334                 set_label();
335
336                 c.fill = GridBagConstraints.NONE;
337                 c.anchor = GridBagConstraints.CENTER;
338                 c.insets = i;
339                 c.weightx = 1;
340                 c.weighty = 1;
341
342                 c.gridx = 0;
343                 c.gridy = 0;
344                 c.gridwidth = 2;
345                 c.anchor = GridBagConstraints.CENTER;
346
347                 pane.add(scanning_label, c);
348
349                 list = new JList(results) {
350                                 //Subclass JList to workaround bug 4832765, which can cause the
351                                 //scroll pane to not let the user easily scroll up to the beginning
352                                 //of the list.  An alternative would be to set the unitIncrement
353                                 //of the JScrollBar to a fixed value. You wouldn't get the nice
354                                 //aligned scrolling, but it should work.
355                                 public int getScrollableUnitIncrement(Rectangle visibleRect,
356                                                                       int orientation,
357                                                                       int direction) {
358                                         int row;
359                                         if (orientation == SwingConstants.VERTICAL &&
360                                             direction < 0 && (row = getFirstVisibleIndex()) != -1) {
361                                                 Rectangle r = getCellBounds(row, row);
362                                                 if ((r.y == visibleRect.y) && (row != 0))  {
363                                                         Point loc = r.getLocation();
364                                                         loc.y--;
365                                                         int prevIndex = locationToIndex(loc);
366                                                         Rectangle prevR = getCellBounds(prevIndex, prevIndex);
367
368                                                         if (prevR == null || prevR.y >= r.y) {
369                                                                 return 0;
370                                                         }
371                                                         return prevR.height;
372                                                 }
373                                         }
374                                         return super.getScrollableUnitIncrement(
375                                                 visibleRect, orientation, direction);
376                                 }
377                         };
378
379                 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
380                 list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
381                 list.setVisibleRowCount(-1);
382
383                 list.addMouseListener(new MouseAdapter() {
384                                  public void mouseClicked(MouseEvent e) {
385                                          if (e.getClickCount() == 2) {
386                                                  monitor_button.doClick(); //emulate button click
387                                          }
388                                  }
389                         });
390                 JScrollPane listScroller = new JScrollPane(list);
391                 listScroller.setPreferredSize(new Dimension(400, 80));
392                 listScroller.setAlignmentX(LEFT_ALIGNMENT);
393
394                 //Create a container so that we can add a title around
395                 //the scroll pane.  Can't add a title directly to the
396                 //scroll pane because its background would be white.
397                 //Lay out the label and scroll pane from top to bottom.
398                 JPanel listPane = new JPanel();
399                 listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS));
400
401                 JLabel label = new JLabel("Select Device");
402                 label.setLabelFor(list);
403                 listPane.add(label);
404                 listPane.add(Box.createRigidArea(new Dimension(0,5)));
405                 listPane.add(listScroller);
406                 listPane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
407
408                 c.fill = GridBagConstraints.BOTH;
409                 c.anchor = GridBagConstraints.CENTER;
410                 c.insets = i;
411                 c.weightx = 1;
412                 c.weighty = 1;
413
414                 c.gridx = 0;
415                 c.gridy = 1;
416                 c.gridwidth = 2;
417                 c.anchor = GridBagConstraints.CENTER;
418
419                 pane.add(listPane, c);
420
421                 cancel_button = new JButton("Cancel");
422                 cancel_button.addActionListener(this);
423                 cancel_button.setActionCommand("cancel");
424
425                 c.fill = GridBagConstraints.NONE;
426                 c.anchor = GridBagConstraints.CENTER;
427                 c.insets = i;
428                 c.weightx = 1;
429                 c.weighty = 1;
430
431                 c.gridx = 0;
432                 c.gridy = 2;
433                 c.gridwidth = 1;
434                 c.anchor = GridBagConstraints.CENTER;
435
436                 pane.add(cancel_button, c);
437
438                 monitor_button = new JButton("Monitor");
439                 monitor_button.addActionListener(this);
440                 monitor_button.setActionCommand("monitor");
441
442                 c.fill = GridBagConstraints.NONE;
443                 c.anchor = GridBagConstraints.CENTER;
444                 c.insets = i;
445                 c.weightx = 1;
446                 c.weighty = 1;
447
448                 c.gridx = 1;
449                 c.gridy = 2;
450                 c.gridwidth = 1;
451                 c.anchor = GridBagConstraints.CENTER;
452
453                 pane.add(monitor_button, c);
454
455                 pack();
456                 setLocationRelativeTo(owner);
457
458                 addWindowListener(new ConfigListener(this));
459
460                 setVisible(true);
461         }
462 }