altosuilib: Show GPS instead of (missing) flight data for TeleGPS graphs
[fw/altos] / altosuilib / 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 org.altusmetrum.altosuilib_2;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import javax.swing.*;
23 import javax.swing.event.*;
24 import java.io.*;
25 import java.util.*;
26 import java.text.*;
27 import java.util.concurrent.*;
28 import org.altusmetrum.altoslib_4.*;
29
30 class AltosScanResult {
31         String          callsign;
32         int             serial;
33         int             flight;
34         AltosFrequency  frequency;
35         int             telemetry;
36
37         boolean interrupted = false;
38
39         public String toString() {
40                 return String.format("%-9.9s serial %-4d flight %-4d (%s %s)",
41                                      callsign, serial, flight, frequency.toShortString(), AltosLib.telemetry_name(telemetry));
42         }
43
44         public String toShortString() {
45                 return String.format("%s %d %d %7.3f %d",
46                                      callsign, serial, flight, frequency, telemetry);
47         }
48
49         public AltosScanResult(String in_callsign, int in_serial,
50                                int in_flight, AltosFrequency in_frequency, int in_telemetry) {
51                 callsign = in_callsign;
52                 serial = in_serial;
53                 flight = in_flight;
54                 frequency = in_frequency;
55                 telemetry = in_telemetry;
56         }
57
58         public boolean equals(AltosScanResult other) {
59                 return (serial == other.serial &&
60                         frequency.frequency == other.frequency.frequency &&
61                         telemetry == other.telemetry);
62         }
63
64         public boolean up_to_date(AltosScanResult other) {
65                 if (flight == 0 && other.flight != 0) {
66                         flight = other.flight;
67                         return false;
68                 }
69                 if (callsign.equals("N0CALL") && !other.callsign.equals("N0CALL")) {
70                         callsign = other.callsign;
71                         return false;
72                 }
73                 return true;
74         }
75 }
76
77 class AltosScanResults extends LinkedList<AltosScanResult> implements ListModel<AltosScanResult> {
78
79         LinkedList<ListDataListener>    listeners = new LinkedList<ListDataListener>();
80
81         void changed(ListDataEvent de) {
82                 for (ListDataListener l : listeners)
83                         l.contentsChanged(de);
84         }
85
86         public boolean add(AltosScanResult r) {
87                 int i = 0;
88                 for (AltosScanResult old : this) {
89                         if (old.equals(r)) {
90                                 if (!old.up_to_date(r))
91                                         changed (new ListDataEvent(this,
92                                                                    ListDataEvent.CONTENTS_CHANGED,
93                                                                    i, i));
94                                 return true;
95                         }
96                         i++;
97                 }
98
99                 super.add(r);
100                 changed(new ListDataEvent(this,
101                                           ListDataEvent.INTERVAL_ADDED,
102                                           this.size() - 2, this.size() - 1));
103                 return true;
104         }
105
106         public void addListDataListener(ListDataListener l) {
107                 listeners.add(l);
108         }
109
110         public void removeListDataListener(ListDataListener l) {
111                 listeners.remove(l);
112         }
113
114         public AltosScanResult getElementAt(int i) {
115                 return this.get(i);
116         }
117
118         public int getSize() {
119                 return this.size();
120         }
121 }
122
123 public class AltosScanUI
124         extends AltosUIDialog
125         implements ActionListener
126 {
127         AltosUIFrame                    owner;
128         AltosDevice                     device;
129         AltosConfigData                 config_data;
130         AltosTelemetryReader            reader;
131         private JList<AltosScanResult>  list;
132         private JLabel                  scanning_label;
133         private JLabel                  frequency_label;
134         private JLabel                  telemetry_label;
135         private JButton                 cancel_button;
136         private JButton                 monitor_button;
137         private JCheckBox[]             telemetry_boxes;
138         javax.swing.Timer               timer;
139         AltosScanResults                results = new AltosScanResults();
140
141         int                             telemetry;
142         boolean                         select_telemetry = false;
143
144         final static int                timeout = 1200;
145         TelemetryHandler                handler;
146         Thread                          thread;
147         AltosFrequency[]                frequencies;
148         int                             frequency_index;
149
150         void scan_exception(Exception e) {
151                 if (e instanceof FileNotFoundException) {
152                         JOptionPane.showMessageDialog(owner,
153                                                       ((FileNotFoundException) e).getMessage(),
154                                                       "Cannot open target device",
155                                                       JOptionPane.ERROR_MESSAGE);
156                 } else if (e instanceof AltosSerialInUseException) {
157                         JOptionPane.showMessageDialog(owner,
158                                                       String.format("Device \"%s\" already in use",
159                                                                     device.toShortString()),
160                                                       "Device in use",
161                                                       JOptionPane.ERROR_MESSAGE);
162                 } else if (e instanceof IOException) {
163                         IOException ee = (IOException) e;
164                         JOptionPane.showMessageDialog(owner,
165                                                       device.toShortString(),
166                                                       ee.getLocalizedMessage(),
167                                                       JOptionPane.ERROR_MESSAGE);
168                 } else {
169                         JOptionPane.showMessageDialog(owner,
170                                                       String.format("Connection to \"%s\" failed",
171                                                                     device.toShortString()),
172                                                       "Connection Failed",
173                                                       JOptionPane.ERROR_MESSAGE);
174                 }
175                 close();
176         }
177
178         class TelemetryHandler implements Runnable {
179
180                 public void run() {
181
182                         boolean interrupted = false;
183
184                         try {
185                                 for (;;) {
186                                         try {
187                                                 AltosState      state = reader.read();
188                                                 if (state == null)
189                                                         continue;
190                                                 if (state.flight != AltosLib.MISSING) {
191                                                         final AltosScanResult   result = new AltosScanResult(state.callsign,
192                                                                                                      state.serial,
193                                                                                                      state.flight,
194                                                                                                      frequencies[frequency_index],
195                                                                                                      telemetry);
196                                                         Runnable r = new Runnable() {
197                                                                         public void run() {
198                                                                                 results.add(result);
199                                                                         }
200                                                                 };
201                                                         SwingUtilities.invokeLater(r);
202                                                 }
203                                         } catch (ParseException pp) {
204                                         } catch (AltosCRCException ce) {
205                                         }
206                                 }
207                         } catch (InterruptedException ee) {
208                                 interrupted = true;
209                         } catch (IOException ie) {
210                         } finally {
211                                 reader.close(interrupted);
212                         }
213                 }
214         }
215
216         void set_label() {
217                 frequency_label.setText(String.format("Frequency: %s", frequencies[frequency_index].toString()));
218                 if (select_telemetry)
219                         telemetry_label.setText(String.format("Telemetry: %s", AltosLib.telemetry_name(telemetry)));
220         }
221
222         void set_telemetry() {
223                 reader.set_telemetry(telemetry);
224         }
225
226         void set_frequency() throws InterruptedException, TimeoutException {
227                 reader.set_frequency(frequencies[frequency_index].frequency);
228                 reader.reset();
229         }
230
231         void next() throws InterruptedException, TimeoutException {
232                 reader.set_monitor(false);
233                 Thread.sleep(100);
234                 ++frequency_index;
235                 if (select_telemetry) {
236                         if (frequency_index >= frequencies.length ||
237                             !telemetry_boxes[telemetry - AltosLib.ao_telemetry_min].isSelected())
238                         {
239                                 frequency_index = 0;
240                                 do {
241                                         ++telemetry;
242                                         if (telemetry > AltosLib.ao_telemetry_max)
243                                                 telemetry = AltosLib.ao_telemetry_min;
244                                 } while (!telemetry_boxes[telemetry - AltosLib.ao_telemetry_min].isSelected());
245                                 set_telemetry();
246                         }
247                 } else {
248                         if (frequency_index >= frequencies.length)
249                                 frequency_index = 0;
250                 }
251                 set_frequency();
252                 set_label();
253                 reader.set_monitor(true);
254         }
255
256
257         void close() {
258                 if (thread != null && thread.isAlive()) {
259                         thread.interrupt();
260                         try {
261                                 thread.join();
262                         } catch (InterruptedException ie) {}
263                 }
264                 thread = null;
265                 if (timer != null)
266                         timer.stop();
267                 setVisible(false);
268                 dispose();
269         }
270
271         void tick_timer() throws InterruptedException, TimeoutException {
272                 next();
273         }
274
275         public void actionPerformed(ActionEvent e) {
276                 String cmd = e.getActionCommand();
277
278                 try {
279                         if (cmd.equals("cancel"))
280                                 close();
281
282                         if (cmd.equals("tick"))
283                                 tick_timer();
284
285                         if (cmd.equals("telemetry")) {
286                                 int k;
287                                 int scanning_telemetry = 0;
288                                 for (k = AltosLib.ao_telemetry_min; k <= AltosLib.ao_telemetry_max; k++) {
289                                         int j = k - AltosLib.ao_telemetry_min;
290                                         if (telemetry_boxes[j].isSelected())
291                                                 scanning_telemetry |= (1 << k);
292                                 }
293                                 if (scanning_telemetry == 0) {
294                                         scanning_telemetry |= (1 << AltosLib.ao_telemetry_standard);
295                                         telemetry_boxes[AltosLib.ao_telemetry_standard - AltosLib.ao_telemetry_min].setSelected(true);
296                                 }
297                                 AltosUIPreferences.set_scanning_telemetry(scanning_telemetry);
298                         }
299
300                         if (cmd.equals("monitor")) {
301                                 close();
302                                 AltosScanResult r = (AltosScanResult) (list.getSelectedValue());
303                                 if (r != null) {
304                                         if (device != null) {
305                                                 if (reader != null) {
306                                                         reader.set_telemetry(r.telemetry);
307                                                         reader.set_frequency(r.frequency.frequency);
308                                                         reader.save_frequency();
309                                                         owner.scan_device_selected(device);
310                                                 }
311                                         }
312                                 }
313                         }
314                 } catch (TimeoutException te) {
315                         close();
316                 } catch (InterruptedException ie) {
317                         close();
318                 }
319         }
320
321         /* A window listener to catch closing events and tell the config code */
322         class ConfigListener extends WindowAdapter {
323                 AltosScanUI     ui;
324
325                 public ConfigListener(AltosScanUI this_ui) {
326                         ui = this_ui;
327                 }
328
329                 public void windowClosing(WindowEvent e) {
330                         ui.actionPerformed(new ActionEvent(e.getSource(),
331                                                            ActionEvent.ACTION_PERFORMED,
332                                                            "close"));
333                 }
334         }
335
336         private boolean open() {
337                 device = AltosDeviceUIDialog.show(owner, AltosLib.product_basestation);
338                 if (device == null)
339                         return false;
340                 try {
341                         reader = new AltosTelemetryReader(new AltosSerial(device));
342                         set_frequency();
343                         set_telemetry();
344                         try {
345                                 Thread.sleep(100);
346                         } catch (InterruptedException ie) {
347                         }
348                         reader.flush();
349                         handler = new TelemetryHandler();
350                         thread = new Thread(handler);
351                         thread.start();
352                         return true;
353                 } catch (FileNotFoundException ee) {
354                         JOptionPane.showMessageDialog(owner,
355                                                       ee.getMessage(),
356                                                       "Cannot open target device",
357                                                       JOptionPane.ERROR_MESSAGE);
358                 } catch (AltosSerialInUseException si) {
359                         JOptionPane.showMessageDialog(owner,
360                                                       String.format("Device \"%s\" already in use",
361                                                                     device.toShortString()),
362                                                       "Device in use",
363                                                       JOptionPane.ERROR_MESSAGE);
364                 } catch (IOException ee) {
365                         JOptionPane.showMessageDialog(owner,
366                                                       device.toShortString(),
367                                                       "Unkonwn I/O error",
368                                                       JOptionPane.ERROR_MESSAGE);
369                 } catch (TimeoutException te) {
370                         JOptionPane.showMessageDialog(owner,
371                                                       device.toShortString(),
372                                                       "Timeout error",
373                                                       JOptionPane.ERROR_MESSAGE);
374                 } catch (InterruptedException ie) {
375                         JOptionPane.showMessageDialog(owner,
376                                                       device.toShortString(),
377                                                       "Interrupted exception",
378                                                       JOptionPane.ERROR_MESSAGE);
379                 }
380                 if (reader != null)
381                         reader.close(false);
382                 return false;
383         }
384
385         public AltosScanUI(AltosUIFrame in_owner, boolean in_select_telemetry) {
386
387                 owner = in_owner;
388                 select_telemetry = in_select_telemetry;
389
390                 frequencies = AltosUIPreferences.common_frequencies();
391                 frequency_index = 0;
392
393                 telemetry = AltosLib.ao_telemetry_standard;
394
395                 if (!open())
396                         return;
397
398                 Container               pane = getContentPane();
399                 GridBagConstraints      c = new GridBagConstraints();
400                 Insets                  i = new Insets(4,4,4,4);
401
402                 timer = new javax.swing.Timer(timeout, this);
403                 timer.setActionCommand("tick");
404                 timer.restart();
405
406                 owner = in_owner;
407
408                 pane.setLayout(new GridBagLayout());
409
410                 scanning_label = new JLabel("Scanning:");
411                 frequency_label = new JLabel("");
412
413                 if (select_telemetry) {
414                         telemetry_label = new JLabel("");
415                         telemetry = AltosLib.ao_telemetry_min;
416                 } else {
417                         telemetry = AltosLib.ao_telemetry_standard;
418                 }
419
420                 set_label();
421
422                 c.fill = GridBagConstraints.HORIZONTAL;
423                 c.anchor = GridBagConstraints.WEST;
424                 c.insets = i;
425                 c.weightx = 1;
426                 c.weighty = 1;
427
428                 c.gridx = 0;
429                 c.gridy = 0;
430                 c.gridwidth = 2;
431
432                 pane.add(scanning_label, c);
433                 c.gridy = 1;
434                 pane.add(frequency_label, c);
435
436                 int     y_offset = 3;
437
438                 if (select_telemetry) {
439                         c.gridy = 2;
440                         pane.add(telemetry_label, c);
441
442                         int     scanning_telemetry = AltosUIPreferences.scanning_telemetry();
443                         telemetry_boxes = new JCheckBox[AltosLib.ao_telemetry_max - AltosLib.ao_telemetry_min + 1];
444                         for (int k = AltosLib.ao_telemetry_min; k <= AltosLib.ao_telemetry_max; k++) {
445                                 int j = k - AltosLib.ao_telemetry_min;
446                                 telemetry_boxes[j] = new JCheckBox(AltosLib.telemetry_name(k));
447                                 c.gridy = 3 + j;
448                                 pane.add(telemetry_boxes[j], c);
449                                 telemetry_boxes[j].setActionCommand("telemetry");
450                                 telemetry_boxes[j].addActionListener(this);
451                                 telemetry_boxes[j].setSelected((scanning_telemetry & (1 << k)) != 0);
452                         }
453                         y_offset += (AltosLib.ao_telemetry_max - AltosLib.ao_telemetry_min + 1);
454                 }
455
456                 list = new JList<AltosScanResult>(results) {
457                                 //Subclass JList to workaround bug 4832765, which can cause the
458                                 //scroll pane to not let the user easily scroll up to the beginning
459                                 //of the list.  An alternative would be to set the unitIncrement
460                                 //of the JScrollBar to a fixed value. You wouldn't get the nice
461                                 //aligned scrolling, but it should work.
462                                 public int getScrollableUnitIncrement(Rectangle visibleRect,
463                                                                       int orientation,
464                                                                       int direction) {
465                                         int row;
466                                         if (orientation == SwingConstants.VERTICAL &&
467                                             direction < 0 && (row = getFirstVisibleIndex()) != -1) {
468                                                 Rectangle r = getCellBounds(row, row);
469                                                 if ((r.y == visibleRect.y) && (row != 0))  {
470                                                         Point loc = r.getLocation();
471                                                         loc.y--;
472                                                         int prevIndex = locationToIndex(loc);
473                                                         Rectangle prevR = getCellBounds(prevIndex, prevIndex);
474
475                                                         if (prevR == null || prevR.y >= r.y) {
476                                                                 return 0;
477                                                         }
478                                                         return prevR.height;
479                                                 }
480                                         }
481                                         return super.getScrollableUnitIncrement(
482                                                 visibleRect, orientation, direction);
483                                 }
484                         };
485
486                 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
487                 list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
488                 list.setVisibleRowCount(-1);
489
490                 list.addMouseListener(new MouseAdapter() {
491                                  public void mouseClicked(MouseEvent e) {
492                                          if (e.getClickCount() == 2) {
493                                                  monitor_button.doClick(); //emulate button click
494                                          }
495                                  }
496                         });
497                 JScrollPane listScroller = new JScrollPane(list);
498                 listScroller.setPreferredSize(new Dimension(400, 80));
499                 listScroller.setAlignmentX(LEFT_ALIGNMENT);
500
501                 //Create a container so that we can add a title around
502                 //the scroll pane.  Can't add a title directly to the
503                 //scroll pane because its background would be white.
504                 //Lay out the label and scroll pane from top to bottom.
505                 JPanel listPane = new JPanel();
506                 listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS));
507
508                 JLabel label = new JLabel("Select Device");
509                 label.setLabelFor(list);
510                 listPane.add(label);
511                 listPane.add(Box.createRigidArea(new Dimension(0,5)));
512                 listPane.add(listScroller);
513                 listPane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
514
515                 c.fill = GridBagConstraints.BOTH;
516                 c.anchor = GridBagConstraints.CENTER;
517                 c.insets = i;
518                 c.weightx = 1;
519                 c.weighty = 1;
520
521                 c.gridx = 0;
522                 c.gridy = y_offset;
523                 c.gridwidth = 2;
524                 c.anchor = GridBagConstraints.CENTER;
525
526                 pane.add(listPane, c);
527
528                 cancel_button = new JButton("Cancel");
529                 cancel_button.addActionListener(this);
530                 cancel_button.setActionCommand("cancel");
531
532                 c.fill = GridBagConstraints.NONE;
533                 c.anchor = GridBagConstraints.CENTER;
534                 c.insets = i;
535                 c.weightx = 1;
536                 c.weighty = 1;
537
538                 c.gridx = 0;
539                 c.gridy = y_offset + 1;
540                 c.gridwidth = 1;
541                 c.anchor = GridBagConstraints.CENTER;
542
543                 pane.add(cancel_button, c);
544
545                 monitor_button = new JButton("Monitor");
546                 monitor_button.addActionListener(this);
547                 monitor_button.setActionCommand("monitor");
548
549                 c.fill = GridBagConstraints.NONE;
550                 c.anchor = GridBagConstraints.CENTER;
551                 c.insets = i;
552                 c.weightx = 1;
553                 c.weighty = 1;
554
555                 c.gridx = 1;
556                 c.gridy = y_offset + 1;
557                 c.gridwidth = 1;
558                 c.anchor = GridBagConstraints.CENTER;
559
560                 pane.add(monitor_button, c);
561
562                 pack();
563                 setLocationRelativeTo(owner);
564
565                 addWindowListener(new ConfigListener(this));
566
567                 setVisible(true);
568         }
569 }