diff options
| author | Keith Packard <keithp@keithp.com> | 2010-05-17 21:30:57 -0700 | 
|---|---|---|
| committer | Keith Packard <keithp@keithp.com> | 2010-05-17 21:30:57 -0700 | 
| commit | 563a9dcdfef42718370c49f16cc2271642b3e055 (patch) | |
| tree | 332c5cc67f8f4fa737691c98c80c9c70b1406dac | |
| parent | cc002c0a43a02845ba67d1a61828be382f307b2e (diff) | |
Finish basic flight monitoring UI with voice using FreeTTS
This captures telemetry data to log files and presents flight status
information in audio form using FreeTTS.
Signed-off-by: Keith Packard <keithp@keithp.com>
| -rw-r--r-- | ao-tools/altosui/AltosFile.java | 37 | ||||
| -rw-r--r-- | ao-tools/altosui/AltosLog.java | 104 | ||||
| -rw-r--r-- | ao-tools/altosui/AltosPreferences.java | 117 | ||||
| -rw-r--r-- | ao-tools/altosui/AltosState.java | 9 | ||||
| -rw-r--r-- | ao-tools/altosui/AltosUI.java | 152 | ||||
| -rw-r--r-- | ao-tools/altosui/AltosVoice.java | 69 | ||||
| -rw-r--r-- | ao-tools/altosui/Makefile | 5 | ||||
| -rw-r--r-- | ao-tools/altosui/voices.txt | 1 | 
8 files changed, 415 insertions, 79 deletions
diff --git a/ao-tools/altosui/AltosFile.java b/ao-tools/altosui/AltosFile.java new file mode 100644 index 00000000..c7ee8679 --- /dev/null +++ b/ao-tools/altosui/AltosFile.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2010 Keith Packard <keithp@keithp.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package altosui; + +import java.lang.*; +import java.io.File; +import java.util.*; +import altosui.AltosTelemetry; +import altosui.AltosPreferences; + +class AltosFile extends File { +	public AltosFile(AltosTelemetry telem) { +		super (AltosPreferences.logdir(), +		       String.format("%04d-%02d-%02d-serial-%03d-flight-%03d.%s", +				     Calendar.getInstance().get(Calendar.YEAR), +				     Calendar.getInstance().get(Calendar.MONTH), +				     Calendar.getInstance().get(Calendar.DAY_OF_MONTH), +				     telem.serial, +				     telem.flight, +				     "telem")); +	} +} diff --git a/ao-tools/altosui/AltosLog.java b/ao-tools/altosui/AltosLog.java new file mode 100644 index 00000000..ec868b9c --- /dev/null +++ b/ao-tools/altosui/AltosLog.java @@ -0,0 +1,104 @@ +/* + * Copyright © 2010 Keith Packard <keithp@keithp.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package altosui; + +import java.io.*; +import java.lang.*; +import java.util.*; +import java.text.ParseException; +import java.util.concurrent.LinkedBlockingQueue; +import altosui.AltosSerial; +import altosui.AltosFile; + +/* + * This creates a thread to capture telemetry data and write it to + * a log file + */ +class AltosLog implements Runnable { + +	LinkedBlockingQueue<String>	input_queue; +	LinkedBlockingQueue<String>	pending_queue; +	int				serial; +	int				flight; +	FileWriter			log_file; +	Thread				log_thread; + +	void close() throws IOException { +		if (log_file != null) +			log_file.close(); +	} + +	boolean open (AltosTelemetry telem) throws IOException { +		AltosFile	a = new AltosFile(telem); + +		log_file = new FileWriter(a, true); +		if (log_file != null) { +			while (!pending_queue.isEmpty()) { +				try { +					String s = pending_queue.take(); +					log_file.write(s); +					log_file.write('\n'); +				} catch (InterruptedException ie) { +				} +			} +			log_file.flush(); +		} +		return log_file != null; +	} + +	public void run () { +		try { +			for (;;) { +				String	line = input_queue.take(); +				try { +					AltosTelemetry	telem = new AltosTelemetry(line); +					if (telem.serial != serial || telem.flight != flight || log_file == null) { +						close(); +						serial = telem.serial; +						flight = telem.flight; +						open(telem); +					} +				} catch (ParseException pe) { +				} +				if (log_file != null) { +					log_file.write(line); +					log_file.write('\n'); +					log_file.flush(); +				} else +					pending_queue.put(line); +			} +		} catch (InterruptedException ie) { +		} catch (IOException ie) { +		} +		try { +			close(); +		} catch (IOException ie) { +		} +	} + +	public AltosLog (AltosSerial s) { +		pending_queue = new LinkedBlockingQueue<String> (); +		input_queue = new LinkedBlockingQueue<String> (); +		s.add_monitor(input_queue); +		serial = -1; +		flight = -1; +		log_file = null; +		log_thread = new Thread(this); +		log_thread.start(); +	} +} diff --git a/ao-tools/altosui/AltosPreferences.java b/ao-tools/altosui/AltosPreferences.java new file mode 100644 index 00000000..0296d935 --- /dev/null +++ b/ao-tools/altosui/AltosPreferences.java @@ -0,0 +1,117 @@ +/* + * Copyright © 2010 Keith Packard <keithp@keithp.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package altosui; +import java.io.*; +import java.util.*; +import java.text.*; +import java.util.prefs.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.awt.Component; +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; + +class AltosPreferences { +	static Preferences preferences; + +	/* logdir preference name */ +	final static String logdirPreference = "LOGDIR"; + +	/* Default logdir is ~/TeleMetrum */ +	final static String logdirName = "TeleMetrum"; + +	/* UI Component to pop dialogs up */ +	static Component component; + +	/* Log directory */ +	static File logdir; + +	public static void init(Component ui) { +		preferences = Preferences.userRoot().node("/org/altusmetrum/altosui"); + +		component = ui; + +		/* Initialize logdir from preferences */ +		String logdir_string = preferences.get(logdirPreference, null); +		if (logdir_string != null) +			logdir = new File(logdir_string); +		else { +			/* Use the file system view default directory */ +			logdir = new File(FileSystemView.getFileSystemView().getDefaultDirectory(), logdirName); +			if (!logdir.exists()) +				logdir.mkdirs(); +		} +	} + +	static void flush_preferences() { +		try { +			preferences.flush(); +		} catch (BackingStoreException ee) { +			JOptionPane.showMessageDialog(component, +						      preferences.absolutePath(), +						      "Cannot save prefernces", +						      JOptionPane.ERROR_MESSAGE); +		} +	} + +	public static void set_logdir(File new_logdir) { +		logdir = new_logdir; +		synchronized (preferences) { +			preferences.put(logdirPreference, logdir.getPath()); +			flush_preferences(); +		} +	} + +	private static boolean check_dir(File dir) { +		if (!dir.exists()) { +			if (!dir.mkdirs()) { +				JOptionPane.showMessageDialog(component, +							      dir.getName(), +							      "Cannot create directory", +							      JOptionPane.ERROR_MESSAGE); +				return false; +			} +		} else if (!dir.isDirectory()) { +			JOptionPane.showMessageDialog(component, +						      dir.getName(), +						      "Is not a directory", +						      JOptionPane.ERROR_MESSAGE); +			return false; +		} +		return true; +	} + +	/* Configure the log directory. This is where all telemetry and eeprom files +	 * will be written to, and where replay will look for telemetry files +	 */ +	public static void ConfigureLog() { +		JFileChooser	logdir_chooser = new JFileChooser(logdir.getParentFile()); + +		logdir_chooser.setDialogTitle("Configure Data Logging Directory"); +		logdir_chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + +		if (logdir_chooser.showDialog(component, "Select Directory") == JFileChooser.APPROVE_OPTION) { +			File dir = logdir_chooser.getSelectedFile(); +			if (check_dir(dir)) +				set_logdir(dir); +		} +	} + +	public static File logdir() { +		return logdir; +	} +} diff --git a/ao-tools/altosui/AltosState.java b/ao-tools/altosui/AltosState.java index 192011d0..59a1999e 100644 --- a/ao-tools/altosui/AltosState.java +++ b/ao-tools/altosui/AltosState.java @@ -29,7 +29,7 @@ public class AltosState {  	/* derived data */ -	double  report_time; +	long  	report_time;  	double	time_change;  	int	tick; @@ -66,11 +66,6 @@ public class AltosState {  	int	speak_tick;  	double	speak_altitude; -	static double -	aoview_time() -	{ -		return System.currentTimeMillis() / 1000.0; -	}  	void init (AltosTelemetry cur, AltosState prev_state) {  		int		i; @@ -82,7 +77,7 @@ public class AltosState {  		ground_altitude = AltosConvert.cc_barometer_to_altitude(data.ground_pres);  		height = AltosConvert.cc_barometer_to_altitude(data.flight_pres) - ground_altitude; -		report_time = aoview_time(); +		report_time = System.currentTimeMillis();  		accel_counts_per_mss = ((data.accel_minus_g - data.accel_plus_g) / 2.0) / 9.80665;  		acceleration = (data.ground_accel - data.flight_accel) / accel_counts_per_mss; diff --git a/ao-tools/altosui/AltosUI.java b/ao-tools/altosui/AltosUI.java index 2952fcc0..43c40799 100644 --- a/ao-tools/altosui/AltosUI.java +++ b/ao-tools/altosui/AltosUI.java @@ -34,6 +34,9 @@ import altosui.AltosSerialMonitor;  import altosui.AltosTelemetry;  import altosui.AltosState;  import altosui.AltosDeviceDialog; +import altosui.AltosPreferences; +import altosui.AltosLog; +import altosui.AltosVoice;  class AltosFlightStatusTableModel extends AbstractTableModel {  	private String[] columnNames = {"Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" }; @@ -128,7 +131,8 @@ public class AltosUI extends JFrame {  	private AltosFlightInfoTableModel[] flightInfoModel;  	private JTable[] flightInfo; -	private AltosSerial serialLine; +	private AltosSerial serial_line; +	private AltosLog altos_log;  	private Box[] ibox;  	private Box vbox;  	private Box hbox; @@ -137,11 +141,15 @@ public class AltosUI extends JFrame {  	private Font infoLabelFont = new Font("SansSerif", Font.PLAIN, 14);  	private Font infoValueFont = new Font("Monospaced", Font.PLAIN, 14); +	public AltosVoice voice = new AltosVoice(); +  	public AltosUI() {  		String[] statusNames = { "Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };  		Object[][] statusData = { { "0", "pad", "-50", "0" } }; +		AltosPreferences.init(this); +  		vbox = Box.createVerticalBox();  		this.add(vbox); @@ -188,7 +196,8 @@ public class AltosUI extends JFrame {  		createMenu(); -		serialLine = new AltosSerial(); +		serial_line = new AltosSerial(); +		altos_log = new AltosLog(serial_line);  		int dpi = Toolkit.getDefaultToolkit().getScreenResolution();  		this.setSize(new Dimension (infoValueMetrics.charWidth('0') * 6 * 20,  					    statusHeight * 4 + infoHeight * 17)); @@ -333,63 +342,76 @@ public class AltosUI extends JFrame {  		info_finish();  	} -	/* User Preferences */ -	Preferences altosui_preferences = Preferences.userNodeForPackage(this.getClass()); - -	/* Log directory */ -	private File logdir = null; +	class IdleThread extends Thread { -	/* logdir preference name */ -	final static String logdirPreference = "LOGDIR"; +		private AltosState state; -	/* Default logdir is ~/TeleMetrum */ -	final static String logdirName = "TeleMetrum"; - -	/* Initialize logdir from preferences */ -	{ -		String logdir_string = altosui_preferences.get(logdirPreference, null); -		if (logdir_string != null) -			logdir = new File(logdir_string); -		else -			/* a hack -- make the file chooser tell us what the default directory -			 * would be and stick our logdir in a subdirectory of that. -			 */ -			logdir = new File(new JFileChooser().getCurrentDirectory(), logdirName); -	} +		public void run () { +			int	reported_landing = 0; -	private void set_logdir(File new_logdir) { -		logdir = new_logdir; -		System.out.printf("Set logdir to %s\n", logdir.toString()); -		synchronized (altosui_preferences) { -			altosui_preferences.put(logdirPreference, logdir.getPath()); +			state = null;  			try { -				altosui_preferences.flush(); -			} catch (BackingStoreException ee) { -				JOptionPane.showMessageDialog(AltosUI.this, -							      altosui_preferences.absolutePath(), -							      "Cannot save prefernces", -							      JOptionPane.ERROR_MESSAGE); +				for (;;) { +					Thread.sleep(10000); +					if (state == null) +						continue; + +					/* reset the landing count once we hear about a new flight */ +					if (state.state < AltosTelemetry.ao_flight_drogue) +						reported_landing = 0; + +					/* Shut up once the rocket is on the ground */ +					if (reported_landing > 2) +						continue; + +					/* If the rocket isn't on the pad, then report height */ +					if (state.state > AltosTelemetry.ao_flight_pad) { +						voice.speak(String.format("%d meters", (int) (state.height + 0.5))); +					} + +					/* If the rocket is coming down, check to see if it has landed; +					 * either we've got a landed report or we haven't heard from it in +					 * a long time +					 */ +					if (!state.ascent && +					    (System.currentTimeMillis() - state.report_time > 10000 || +					     state.state == AltosTelemetry.ao_flight_landed)) +					{ +						if (Math.abs(state.baro_speed) < 20 && state.height < 100) +							voice.speak("rocket landed safely"); +						else +							voice.speak("rocket may have crashed"); +						if (state.gps != null) +							voice.speak(String.format("bearing %d degrees, range %d meters", +										  (int) (state.from_pad.bearing + 0.5), +										  (int) (state.from_pad.distance + 0.5))); +						++reported_landing; +					} +				} +			} catch (InterruptedException ie) {  			}  		} + +		public void notice(AltosState new_state) { +			state = new_state; +		}  	} -	private boolean check_dir(File dir) { -		if (!dir.exists()) { -			if (!dir.mkdirs()) { -				JOptionPane.showMessageDialog(AltosUI.this, -							      dir.getName(), -							      "Cannot create directory", -							      JOptionPane.ERROR_MESSAGE); -				return false; +	private void tell(AltosState state, AltosState old_state) { +		if (old_state == null || old_state.state != state.state) { +			voice.speak(state.data.state); +			switch (state.state) { +			case AltosTelemetry.ao_flight_fast: +				voice.speak(String.format("max speed %d meters per second", +							  (int) (state.max_speed + 0.5))); +				break; +			case AltosTelemetry.ao_flight_drogue: +				voice.speak(String.format("max height %d meters", +							  (int) (state.max_height + 0.5))); +				break;  			} -		} else if (!dir.isDirectory()) { -			JOptionPane.showMessageDialog(AltosUI.this, -						      dir.getName(), -						      "Is not a directory", -						      JOptionPane.ERROR_MESSAGE); -			return false;  		} -		return true; +		old_state = state;  	}  	class DisplayThread extends Thread { @@ -402,16 +424,22 @@ public class AltosUI extends JFrame {  		public void run() {  			String		line;  			AltosState	state = null; +			AltosState	old_state = null; +			IdleThread	idle_thread = new IdleThread();  			info_reset();  			info_finish(); +			idle_thread.start();  			try {  				while ((line = read()) != null) {  					try {  						AltosTelemetry	t = new AltosTelemetry(line); +						old_state = state;  						state = new AltosState(t, state);  						update(state);  						show(state); +						tell(state, old_state); +						idle_thread.notice(state);  					} catch (ParseException pp) {  						System.out.printf("Parse error on %s\n", line);  						System.out.println("exception " + pp); @@ -420,6 +448,7 @@ public class AltosUI extends JFrame {  			} catch (InterruptedException ee) {  			} finally {  				close(); +				idle_thread.interrupt();  			}  		}  	} @@ -450,8 +479,8 @@ public class AltosUI extends JFrame {  		if (device != null) {  			try { -				serialLine.connect(device.tty); -				DeviceThread thread = new DeviceThread(serialLine); +				serial_line.connect(device.tty); +				DeviceThread thread = new DeviceThread(serial_line);  				run_display(thread);  			} catch (FileNotFoundException ee) {  				JOptionPane.showMessageDialog(AltosUI.this, @@ -556,7 +585,7 @@ public class AltosUI extends JFrame {  		logfile_chooser.setDialogTitle("Select Telemetry File");  		logfile_chooser.setFileFilter(new FileNameExtensionFilter("Telemetry file", "telem")); -		logfile_chooser.setCurrentDirectory(logdir); +		logfile_chooser.setCurrentDirectory(AltosPreferences.logdir());  		int returnVal = logfile_chooser.showOpenDialog(AltosUI.this);  		if (returnVal == JFileChooser.APPROVE_OPTION) { @@ -583,23 +612,6 @@ public class AltosUI extends JFrame {  	private void SaveFlightData() {  	} -	/* Configure the log directory. This is where all telemetry and eeprom files -	 * will be written to, and where replay will look for telemetry files -	 */ -	private void ConfigureLog() { -		JFileChooser	logdir_chooser = new JFileChooser(); - -		logdir_chooser.setDialogTitle("Configure Data Logging Directory"); -		logdir_chooser.setCurrentDirectory(logdir.getParentFile()); -		logdir_chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - -		if (logdir_chooser.showDialog(AltosUI.this, "Select Directory") == JFileChooser.APPROVE_OPTION) { -			File dir = logdir_chooser.getSelectedFile(); -			if (check_dir(dir)) -				set_logdir(dir); -		} -	} -  	/* Create the AltosUI menus  	 */  	private void createMenu() { @@ -681,7 +693,7 @@ public class AltosUI extends JFrame {  			item = new JMenuItem("Configure Log",KeyEvent.VK_C);  			item.addActionListener(new ActionListener() {  					public void actionPerformed(ActionEvent e) { -						ConfigureLog(); +						AltosPreferences.ConfigureLog();  					}  				});  			menu.add(item); diff --git a/ao-tools/altosui/AltosVoice.java b/ao-tools/altosui/AltosVoice.java new file mode 100644 index 00000000..e4ea99a2 --- /dev/null +++ b/ao-tools/altosui/AltosVoice.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2010 Keith Packard <keithp@keithp.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package altosui; + +import com.sun.speech.freetts.Voice; +import com.sun.speech.freetts.VoiceManager; +import com.sun.speech.freetts.audio.JavaClipAudioPlayer; +import java.util.concurrent.LinkedBlockingQueue; + +public class AltosVoice implements Runnable { +	VoiceManager			voice_manager; +	Voice				voice; +	LinkedBlockingQueue<String>	phrases; +	Thread				thread; + +	final static String voice_name = "kevin16"; + +	public void run() { +		try { +			for (;;) { +				String s = phrases.take(); +				voice.speak(s); +			} +		} catch (InterruptedException e) { +		} +	} +	public void speak(String s) { +		try { +			if (voice != null) +				phrases.put(s); +		} catch (InterruptedException e) { +		} +	} + +	public AltosVoice () { +		voice_manager = VoiceManager.getInstance(); +		voice = voice_manager.getVoice(voice_name); +		if (voice != null) { +			voice.allocate(); +			phrases = new LinkedBlockingQueue<String> (); +			thread = new Thread(this); +			thread.start(); +			speak("Rocket Flight Monitor Ready"); +		} else { +			System.out.printf("Voice manager failed to open %s\n", voice_name); +			Voice[] voices = voice_manager.getVoices(); +			System.out.printf("Available voices:\n"); +			for (int i = 0; i < voices.length; i++) { +				System.out.println("    " + voices[i].getName() +						   + " (" + voices[i].getDomain() + " domain)"); +			} +		} +	} +} diff --git a/ao-tools/altosui/Makefile b/ao-tools/altosui/Makefile index b9f025da..57c889b8 100644 --- a/ao-tools/altosui/Makefile +++ b/ao-tools/altosui/Makefile @@ -1,6 +1,6 @@  .SUFFIXES: .java .class -CLASSPATH=..:/usr/share/java/* +CLASSPATH=..:/usr/share/java/*:/home/keithp/src/freetts/freetts-1.2.2  CLASSFILES=\  	AltosConvert.class \  	AltosFile.class \ @@ -16,7 +16,8 @@ CLASSFILES=\  	AltosUI.class \  	AltosDevice.class \  	AltosDeviceLinux.class \ -	AltosDeviceDialog.class +	AltosDeviceDialog.class \ +	AltosVoice.class  JAVAFLAGS=-Xlint:unchecked diff --git a/ao-tools/altosui/voices.txt b/ao-tools/altosui/voices.txt new file mode 100644 index 00000000..e8825fc3 --- /dev/null +++ b/ao-tools/altosui/voices.txt @@ -0,0 +1 @@ +com.sun.speech.freetts.en.us.cmu_us_kal.KevinVoiceDirectory  | 
