Archive

Monthly Archives: September 2013

I wrote a while ago about a mechanism to locate and connect to a headless Raspberry Pi over Ethernet using an LCD display and some start-up code.

Well today I broke it while preparing to move house (and use it in it’s intended situation!), which was bad news. Listen to your GND markings, people!

But a moment’s search for a replacement strategy yielded another idea. Nothing original by any means, but something new to my programming adventures thus far: Get the IP address by e-mail on boot!

Looking at a Raspberry Pi as it boots you will see the Ethernet port is initialized pretty early on in the boot procedure. A quick Google search revealed the existence of the ‘smtplib‘ module included with Python, which I leveraged to make this happen. Here is the final code (get_ip_address() found here):

import smtplib
import struct
import socket
import fcntl

msg = "From RPi Python"

def get_ip_address(ifname):
	s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	return socket.inet_ntoa(fcntl.ioctl(
		s.fileno(), 0x8915, # SIOCGIFADDR
		struct.pack('256s', ifname[:15])
	)[20:24])

fromaddr = <from address>
toaddr = <to address>

msg = """RPi IP is  %s""" % get_ip_address('eth0')

username = <username>
password = <password>

print("Sending IP: %s to %s" % (get_ip_address('eth0'), toaddr))

print("Opening connection...")
server = smtplib.SMTP('smtp.gmail.com:587')
server.ehlo()
server.starttls()
server.ehlo()

print("Logging in...")
server.login(username, password)

print("Sending message: %s" % msg)
server.sendmail(fromaddr, toaddr, msg)

print("Closing connection...")
server.close()

print("Sent.")

The next step is to make it run on boot, which only involved writing this script (called ipmailboot.sh):

#!/usr/bin/python

sudo python ~/python/ipmail.py

Then changing the permissions for execution:

 sudo chmod 755 ipmailboot.sh
 

And then registering it to run on boot using update-rc.d ipmailboot.sh defaults.

So, nothing revolutionary, but when I move house and find the router is nowhere near a TV, all I’ll have to do is connect it to the network with an Ethernet cable, power on and wait for the email to arrive (on my Pebble watch, no less!)

logosrcIntroduction

After the success of the first Watch Trigger, and a good deal of requests, I spent another two weeks to build upon and improve the experience. The result of all this work is Watch Trigger + (or Plus), and boasts this list of improvements:

  • Remote triggering of video capture, as well as photo capture
  • New landing screen for mode selection and a single access point to the Settings menu
  • Settings menu has been re-vamped to enable expansion with new settings in the future using PreferenceHeaders.
  • Included Gingerbread devices in the Media Gallery scanning functionality, as there were problems previously. Gingerbread devices will use Intent driven media scanning instead of Honeycomb + devices using a MediaScanner connection and callback method.
  • File names are now based on the time and date they were taken.
  • Removed watch-app autostart when entering either of the viewfinder Activities. This approach led to some AppSync difficulties with the improvements in the next bullet point;
  • Enhanced Watch App that adapts its layout depending on which shooting mode the Android app is currently in.
  • More stability fixes, including slightly faster Photo Viewfinder startup time.

Screenshots

The new landing screen for mode selection:

shot1This Activity is fitted with some smooth and subtle animations to make it feel a lot nicer to use. Also note the single access point to the Settings menu on the ActionBar at the top right.

The new Video Viewfinder:

shot3Mostly similar to the Photo Viewfinder, but lacking the timer UI, as it is rendered pretty much moot by definition, as the video captured can be of any length.

The new enhanced adapting watch app:

WTP watchappExcuse the state of my screen protector! So far it’s done its duty perfectly. I haven’t found any function to assign the UP and DOWN buttons on the Pebble Action Bar so far, so if you can think of one, let me know!

Notes on Android

First, the process for capturing video on Android is very different from photo capture. There are two methods I can think of for capturing photos/videos on Android:

  1. Start an Intent to launch the device’s built-in Camera app, which then waits for the user to press the capture button and then go back, which hands the resulting image data back to the previous Activity. Useless for this purpose, since once the Intent is launched, the Watch Trigger app and hence Pebble cannot control the built-in Camera app, which leaves us with the alternative;
  2. Re-implement the Camera app as a custom Activity to enable access to all stages of preview, capture, write and gallery scan. This involves creating a new SurfaceView subclass that opens the Camera and displays the preview images. Once the basic layout is complete, and the Camera.Parameters API probed to expose the requires settings to the user, this isn’t too much work.
The problems start to appear if you want to do approach #2 above with video capture. Whereas the Camera API has the takePicture() method, which calls the supplied callbacks to get and save the image data to internal storage, the capturing of video data requires continuous storage functionality, which is managed with the MediaRecorder class.
On paper (in the API documentation), the video capture process is simple enough, if you tread with caution. Even following this admittedly precision orientated procedure, I spent at least two days wrestling with ‘setParameters failed’ and ‘start failed -19’ errors. One thing I like about the Java language is with a suitable debugger the stack trace is nearly always informative enough to show you exactly what failed and why. But these errors were near meaningless, and according to sites such as Stack Overflow, could occur due to a wide variety of reasons.
Eventually I managed to get video capture to work after making assumptions about camera hardware, encoder options and file formats, which when considering to release to a device-fragmented ecosystem such as Android, is scary enough. A few more days work enabled me to eliminate most of these assumptions which should provide the best compatibility. In case you were led here by struggles re-creating the Camera app for video recording, here is my code which works (at least for a CM10.1 Galaxy S, stock HTC One, stock 2.3.3 Galaxy Ace and stock Galaxy Y (I still pity Galaxy Y users):
	/**
	 * THANK YOU: http://stackoverflow.com/a/17397920
	 * @return true if successful
	 */
	private boolean prepareMediaRecorder() {
		try {
			//Create
			mRecorder = new MediaRecorder();

			//Select camera
			mRecorder.setCamera(camera);
			if(Globals.DEBUG_MODE)
				Log.d(TAG, "Camera instance is: " + camera.toString());

			//Setup audio/video sources
			mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
			mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

			//Set quality
			CamcorderProfile profile = CamcorderProfile.get(0, CamcorderProfile.QUALITY_HIGH);
			mRecorder.setProfile(profile);

			//Get next name
			nextName = getTimeStampName() + ".mp4";

			//Output file
			if(Globals.DEBUG_MODE)
				Log.d(TAG, "Opening media file...");
			File dir = new File (prefPath + "/");
			dir.mkdirs();
			currentFile = new File(dir, nextName);

			if(Globals.DEBUG_MODE)
				Log.d(TAG, "Media file is: " + currentFile.getAbsolutePath().toString());
			mRecorder.setOutputFile(currentFile.getAbsolutePath().toString());

			//Setup Surface
			mRecorder.setPreviewDisplay(sHolder.getSurface());

			//Prepare
			if(Globals.DEBUG_MODE)
				Log.d(TAG, "Preparing MediaRecorder...");
			mRecorder.prepare();

			//Finally
			if(Globals.DEBUG_MODE)
				Log.d(TAG, "MediaRecorder preparations complete!");
			Globals.addToDebugLog(TAG, "MediaRecorder preparations complete!");
			return true;
		} catch (Exception e) {
			Log.e(TAG, "Error preparing MediaRecorder: " + e.getLocalizedMessage());
			Globals.addToDebugLog(TAG, "Error preparing MediaRecorder: " + e.getLocalizedMessage());
			e.printStackTrace();
			releaseMediaRecorder();
			return false;
		}
	}

	private void releaseMediaRecorder() {
		mRecorder.reset();
		mRecorder.release();
		mRecorder = null;

		if(Globals.DEBUG_MODE)
			Log.d(TAG, "MediaRecorder released successfully.");
		Globals.addToDebugLog(TAG, "MediaRecorder released successfully");
	}

If you are working in this area of Android app development heed my warning and ALWAYS USE TRY/CATCH TO RELEASE THE CAMERA LOCK AND MEDIARECORDER LOCK IF ANY CODE SEGMENT INVOLVING THEM FAILS! Failure to do this means if your app FCs or ANRs and you have to kill it, you will be unable to access the Camera in ANY app until you restart your device!

Finally in this section, notes on supporting Android 2.3.3 Gingerbread and upwards. In Android 3.0 Honeycomb and upwards, there are a lot of nice features and conveniences I originally took for granted when building this app. Examples include:

  • The ActionBar API
  • The newer Media Scanner API functions
  • Some methods involved with the Camera API

After a few requests and accepting that I should support all the devices that Pebble do themselves, I worked to include those older devices into the Watch Trigger fold. In doing so, I had to write replacement imitation ActionBar layout items and buttons to provide the closest possible similarity between device versions. Originally I had great difficulties with implementing media scanning (to add the captures media files to the system Gallery so they can be viewed immediately) on Gingerbread, but no problems with Honeycomb upwards. I got round this like so:

	//Check Android version
	public static boolean isHoneycombPlus() {
		return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB;
	}

.............................................................

	if(Globals.isHoneycombPlus()) {
		MediaScannerConnection.scanFile(getContext(), paths, mimes, new MediaScannerConnection.OnScanCompletedListener() {

			@Override
			public void onScanCompleted(String path, Uri uri) {
				if(Globals.DEBUG_MODE)
					Log.d(TAG, "Finished scanning " + path + " to gallery");
				Globals.addToDebugLog(TAG, "Finished scanning video into Gallery");
				VideoViewfinder.overlayNotify("Media scan complete.");

				//Finally
				readyToCapture = true;
			}

		});
	} else {
		//Media scan intent?
		Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
		intent.setData(Uri.fromFile(currentFile));
		getContext().sendBroadcast(intent);

		VideoViewfinder.overlayNotify("Media scan requested.");

		//Finally
		readyToCapture = true;
	}

Thus, in many other places including the one shown above, the app takes a different path depending on the device platform version.

So, that’s the big upgrade! All that’s left now is to provide a link to get your teeth into taking loony videos of yourself. Hopefully nothing like this.

Download


Get it on Google Play

icon

The latest update on it’s way to the Google Play store now is the largest yet, so I thought I’d go through it in more detail.

The first major change is the return of support for Android 2.3.3 Gingerbread and up. Previously this was 3.0 Honeycomb and up due to the use of the surprisingly useful ActionBar and PreferenceFragment APIs, used to add buttons to the nice red stripe at the top of the viewfinder (as well as enable it’s opacity) and automatic creation of the Settings menu layout from an XML file respectively.

Using the above features meant I had to restrict the app to 3.0+ users, which I was uncomfortable doing, seeing as that is the benchmark set for compatibility by the official Pebble app. But since then and after requests from a few would-be users, I worked to create an app that works differently based on the user’s version of Android.

If the user is on 3.0 and up, it will use the ActionBar, add the watch-app toggle and settings buttons as ActionBar options, and use the PreferenceFragment API to generate the Settings menu when it is opened by the user. However, if the user is on 2.3.3 (but below 3.0), the app will use a replacement viewfinder layout that replicates the ActionBar layout as close as I can. I must say, I’m impressed at the result. For the Settings menu, the older deprecated PreferencesActivity class is used to generate the settings instead.

Unfortunately, despite trying hard, I had to take out automatic scanning of captures images to the Gallery on Gingerbread. I just couldn’t crack it for now. I’ll keep trying though.

Here’s how the new viewfinder looks with the progressbar, in my room, as it’s currently very dark outside…

shot1

shot2

Other new features include:

  1. Subtle animations on a couple of UI elements; namely the reset button (if Instant Review is on) and the countdown timer remaining TextView, when it is visible
  2. A ProgressBar showing the stages of trigger, capture, saving and media scanning of the image. If Instant Review is on, the reset button appears once this process is complete, ensuring users don’t reset the camera before the image is finished saving.
  3. The timer now has an option between 1 and 5 second increments (but still up to a 15 second maximum).
  4. Full control over the image save path. This is set by default to the device’s variant on ‘storage/sdcard0/’ or ‘mnt/sdcard/’, whatever Environment.getExternalStorageDirectory() returns. After this, the user is free to change this, even to go up and save to another attached media instead, such as an external SD card, which on my device would be ‘storage/sdcard1/’.
  5. A guard mechanism to protect against launching the app when the phone is mounted as a removable USB device on a PC. Seeing as the app’s sole purpose is to save images to the internal media (which is unavailable when the phone is mounted), this mechanism is a no-brainer.
  6. Finally, a wake-lock so that the phone doesn’t go to sleep while you are arranging your photo/relatives.

I think that’s all, so have at it and enjoy! The in-app feedback options are still there in case anything goes wrong.

One other note: I’ve had reports of intermittent responses from the watch-app, such as UP and DOWN buttons working, but not SELECT. Seeing as I’ve implemented the Pebble AppSync as best as I am able, all events go through the same procedures, so receive equal treatment. This unusual behavior could be attributed to the on-going battle Pebble are fighting against flaky device connections as one of the focuses behind the last few Pebble App updates. As a personal response to this, I am working on my own abstraction above AppSync (which is itself an abstraction of AppMessage) to try and get a better handle on the continuous state of the connection to the watch.

More info if that gets anywhere.

Download
Get it on Google Play