DJI Battery Monitoring

DJI charges a significant amount of money for their batteries and calls them Smart Batteries. I’ve seen the statistics reported of how many times the battery has been power cycled, as well as details of how much flight time is available. This was a new and interesting feature to me.

Screenshot_20160502-075201

I had left the drone in the back of my car overnight. The temperature had probably gotten into the high 30s, and was still in the mid 50s with the sun shining. The warning message “Battery Temperature Too Low. Warm battery to at least 15 degrees Celcius before flying” came up on my screen and would not let me initiate a takeoff.

I have used batteries in cold climates in the past. I know how temperature affects both current output from batteries and future usability. I’m impressed that DJI has built in this feature to their firmware.

Flashing ESCs on Hobbylord BumbleBee

I bought a Bumblebee Quad from a local hobby shop a few months ago, and when I finally got around to trying to build it with a proper autopilot found that it’s ESCs used a protocol called UltraPWM that is a very uncommon protocol.

I came across this page, https://github.com/sim-/tgy/issues/13 , which leads me to believe that I should be able to flash the ESCs with a simonk tgy firmware and use the hardware I already have.

I came across a cable from hobbyking that is designed to make contact with the surface mounted atmel device and allow programming without any soldering or desoldering. http://hobbyking.com/hobbyking/store/__27195__Atmel_Atmega_Socket_Firmware_Flashing_Tool.html It was designed to be used with an atmel programming device that they also sell http://hobbyking.com/hobbyking/store/__27990__USBasp_AVR_Programming_Device_for_ATMEL_proccessors.html and so I thought I’d be good to go. The cable cost $20 while the programmer cost $4, but not needing to solder anything was a very positive solution for me.

USBasp AVR Programming Device for ATMEL proccessors

USBasp AVR Programming Device

Atmel Atmega Socket Firmware Flashing Tool

Atmel Atmega Socket Firmware Flashing Tool

What I didn’t recognize until it all arrived was that Hobbyking has updated the USB Programmer to use a 6 conductor connector, but not updated their programming cable from the 10 conductor cable. The message boards on hobbyking discuss the change, and have pinout descriptions, but it’s been very frustrating because getting the parts to do the correct wiring has not been as simple as plug and play.

Atmega contact points

Atmega contact points

Atmega contact points

Atmega contact points

Cable Pinout Description

Cable Pinout Description

This has been extremely frustrating to me as the parts I ordered were billed as no soldering required, but could not be simply plugged into each other.

File a flight plan before takeoff

Always think through what you are going to do before you take off.

Broken XHeli

Broken Quadcopter

I had decided that I wanted to try the Follow-Me option in AndroPilot. Turned on my Taranis transmitter, but then left it sitting on the bench with the throttle down, and all the switches in default positions. I connected the 3DR radio to my Google Nexus 7 running AndoPilot. I hit the button on the tablet to arm the drone, and took it off entirely from the tablet. I was flying using the virtual sticks on the screen of the tablet, which was not extremely intuitive because there’s no tactile feedback on the tablet. I’d raised the drone up to about 40 feet, and was trying to find the button to attempt follow me.  It didn’t appear to be moving towards me, or doing anything very predictable. My friend near the bench asked what it took to take control from the RC transmitter, and I said just start flipping switches.  I continued to fly for about another 30 seconds when I asked him to flip return to launch. (I’ve got that set on a switch of it’s own, using channel 7 of the transmitter.)  He was getting concerned because I’d drifted halfway to the trees, and putting the copter in the trees 40 feet up was likely to be a total loss. He flipped the flight mode switch going to loiter. The copter stopped and tumbled out of the sky.

Afterwards he pointed out that before a flight we really needed to discuss what was supposed to happen, and what he could do to recover before the flight begins.

The reason the copter fell out of the sky was that he changed the flight mode without moving the throttle from zero, so when the copter took over it dropped the throttle and fell out of the sky. If he’d just changed the RTL switch things probably would have been just fine. My transmitter is set differently from his. I’ve got all 8 channels configured, with channel 7 being it’s own switch that only deals with the RTL command. He’s got an older firmware and uses RTL as one of his flight modes.

I asked for one thing to happen, and he did something else. He didn’t know exactly what I wanted, and by the time I needed it I wasn’t in a position to explain it quickly. The broken parts can be replaced for $15 from the original place I bought my X, or I can use another supplier and get other parts. I’ve bought an entirely second frame already with nothing mounted on it for only $12 so I might go that route instead of ordering more parts, or I may move parts to run on my new BumbleBee platform.

Media Center Content Protection Error

I’ve noticed that I sometimes get errors in windows media center reporting that programs could not be recorded. This generally happens when I’ve left media center running full screen on one of my monitors overnight…

MediaCenterErrorThe strange thing is that if the media center application is not running, or is running in a window, the recording of the programs seems to work properly. I only get these errors coming up when the application is full screen on one of my monitors.

 

Time-lapse videos from GoPro

In November 2013 I purchased a GoPro HERO3+ Black Edition to play with video recording, but almost immediately became enthralled with taking sequences of photos over long time periods.

The GoPro can be configured to take a picture every 0.5, 1, 2, 5, 10, 30, or 60 seconds. I’ve found that I like taking pictures every 2 seconds, and then converting them to video at 30fps (Frames per second) which gets me an easy to convert time scale. 1 second of video came from 1 minute of photos, 1 minute of video came from 1 hour of photos.

GoPro has a freely available software package to edit videos, as well as creating videos from sequences of images. Because of my past familiarity with FFMPEG I wanted a more scriptable solution for creating videos from thousands of photos.

https://trac.ffmpeg.org/wiki/Create%20a%20video%20slideshow%20from%20images has nice instructions for creating videos from sequences of images using FFMPEG. What it glosses over is that the first image in the sequence needs to be numbered zero or one. Another complication in the process is that the GoPro uses the standard camera file format where no more than 1000 images will be stored in a single directory. This means that with the 1800 images created in a single hour, at least two directories will hold the source images. An interesting issue I ran across is that sometimes the GoPro will skip a number in its image sequence, especially when it has just moved to the next directory in sequence. This is why I had to write my program using directory listings as opposed to simply looking for known files.

The standard GoPro battery will record just about two hours worth of photos. If the GoPro is connected to an external power supply, you can be limited only by the amount of storage space.

Here’s yesterday morning’s weather changing in Seattle.

Here’s a comparison of cropping vs compressing the video. I took this video on a flight from Seattle to Pullman last weekend. You can see much more of the landscape in the compressed version, and see that the top of the propeller leaves the frame in the cropped version.

Compressed:


Cropped:

I’ve written a program that takes three parameters, copies all of the images to a temporary location with an acceptable filename sequence, runs FFMPEG to create a video, then deletes the temporary images. The GoPro is configured to take full resolution still frames, 4000×3000, and I convert those to a 1080p video format using FFMPEG. Because the aspect ratio is different, and the GoPro uses a fish eye lens to begin with, both vertical and horizontal distortion shows up. I run FFMPEG twice, once creating a compressed video and a second time creating a cropped video. This allows me to chose which level of distortion I prefer after the fact.

The three parameters are the video name, the first image in the sequence, and the last image in the sequence. I am currently doing very little error checking. I’m presenting this code here, just to document what I’ve done so far. If you find this useful, please let me know.

Here’s some helper functions I regularly use.

using namespace std;

/////////////////////////////////////////////////////////////////////////////
CString FindEXEFromPath(const CString & csEXE)
{
	CString csFullPath;
	CFileFind finder;
	if (finder.FindFile(csEXE))
	{
		finder.FindNextFile();
		csFullPath = finder.GetFilePath();
		finder.Close();
	}
	else
	{
		TCHAR filename[MAX_PATH];
		unsigned long buffersize = sizeof(filename) / sizeof(TCHAR);
		// Get the file name that we are running from.
		GetModuleFileName(AfxGetResourceHandle(), filename, buffersize );
		PathRemoveFileSpec(filename);
		PathAppend(filename, csEXE);
		if (finder.FindFile(filename))
		{
			finder.FindNextFile();
			csFullPath = finder.GetFilePath();
			finder.Close();
		}
		else
		{
			CString csPATH;
			csPATH.GetEnvironmentVariable(_T("PATH"));
			int iStart = 0;
			CString csToken(csPATH.Tokenize(_T(";"), iStart));
			while (csToken != _T(""))
			{
				if (csToken.Right(1) != _T("\\"))
					csToken.AppendChar(_T('\\'));
				csToken.Append(csEXE);
				if (finder.FindFile(csToken))
				{
					finder.FindNextFile();
					csFullPath = finder.GetFilePath();
					finder.Close();
					break;
				}
				csToken = csPATH.Tokenize(_T(";"), iStart);
			}
		}
	}
	return(csFullPath);
}
/////////////////////////////////////////////////////////////////////////////
static const CString QuoteFileName(const CString & Original)
{
	CString csQuotedString(Original);
	if (csQuotedString.Find(_T(" ")) >= 0)
	{
		csQuotedString.Insert(0,_T('"'));
		csQuotedString.AppendChar(_T('"'));
	}
	return(csQuotedString);
}
/////////////////////////////////////////////////////////////////////////////
std::string timeToISO8601(const time_t & TheTime)
{
	std::ostringstream ISOTime;
	struct tm UTC;// = gmtime(&timer);
	if (0 == gmtime_s(&UTC, &TheTime))
	{
		ISOTime.fill('0');
		ISOTime << UTC.tm_year+1900 << "-";
		ISOTime.width(2);
		ISOTime << UTC.tm_mon+1 << "-";
		ISOTime.width(2);
		ISOTime << UTC.tm_mday << "T";
		ISOTime.width(2);
		ISOTime << UTC.tm_hour << ":";
		ISOTime.width(2);
		ISOTime << UTC.tm_min << ":";
		ISOTime.width(2);
		ISOTime << UTC.tm_sec;
	}
	return(ISOTime.str());
}
std::wstring getTimeISO8601(void)
{
	time_t timer;
	time(&timer);
	std::string isostring(timeToISO8601(timer));
	std::wstring rval;
	rval.assign(isostring.begin(), isostring.end());
	
	return(rval);
}
/////////////////////////////////////////////////////////////////////////////

Here’s a routine I found useful to parse the standard camera file system naming format.

/////////////////////////////////////////////////////////////////////////////
bool SplitImagePath(
	CString csSrcPath,
	CString & DestParentDir,
	int & DestChildNum,
	CString & DestChildSuffix,
	CString & DestFilePrefix,
	int & DestFileNumDigits,
	int & DestFileNum,
	CString & DestFileExt
	)
{
	bool rval = true;
	DestFileExt.Empty();
	while (csSrcPath[csSrcPath.GetLength()-1] != _T('.'))
	{
		DestFileExt.Insert(0, csSrcPath[csSrcPath.GetLength()-1]);
		csSrcPath.Truncate(csSrcPath.GetLength()-1);
	}
	csSrcPath.Truncate(csSrcPath.GetLength()-1); // get rid of dot

	CString csDestFileNum;
	DestFileNumDigits = 0;
	while (iswdigit(csSrcPath[csSrcPath.GetLength()-1]))
	{
		csDestFileNum.Insert(0, csSrcPath[csSrcPath.GetLength()-1]);
		DestFileNumDigits++;
		csSrcPath.Truncate(csSrcPath.GetLength()-1);
	}
	DestFileNum = _wtoi(csDestFileNum.GetString());

	DestFilePrefix.Empty();
	while (iswalpha(csSrcPath[csSrcPath.GetLength()-1]))
	{
		DestFilePrefix.Insert(0, csSrcPath[csSrcPath.GetLength()-1]);
		csSrcPath.Truncate(csSrcPath.GetLength()-1);
	}
	csSrcPath.Truncate(csSrcPath.GetLength()-1); // get rid of backslash

	DestChildSuffix.Empty();
	while (iswalpha(csSrcPath[csSrcPath.GetLength()-1]))
	{
		DestChildSuffix.Insert(0, csSrcPath[csSrcPath.GetLength()-1]);
		csSrcPath.Truncate(csSrcPath.GetLength()-1);
	}

	CString csDestChildNum;
	while (iswdigit(csSrcPath[csSrcPath.GetLength()-1]))
	{
		csDestChildNum.Insert(0, csSrcPath[csSrcPath.GetLength()-1]);
		csSrcPath.Truncate(csSrcPath.GetLength()-1);
	}
	DestChildNum = _wtoi(csDestChildNum.GetString());

	DestParentDir = csSrcPath;
	return(rval);
}
/////////////////////////////////////////////////////////////////////////////

And here’s the main program.

/////////////////////////////////////////////////////////////////////////////
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
	int nRetCode = 0;

	HMODULE hModule = ::GetModuleHandle(NULL);

	if (hModule != NULL)
	{
		// initialize MFC and print and error on failure
		if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0))
		{
			// TODO: change error code to suit your needs
			_tprintf(_T("Fatal Error: MFC initialization failed\n"));
			nRetCode = 1;
		}
		else
		{
			CString csFFMPEGPath(FindEXEFromPath(_T("ffmpeg.exe")));
			CString csFirstFileName;
			CString csLastFileName;
			CString csVideoName;

			if (argc != 4)
			{
				std::wcout << "command Line Format:" << std::endl;
				std::wcout << "\t" << argv[0] << " VideoName PathToFirstFile.jpg PathToLastFile.jpg" << std::endl;
			}
			else
			{
				csVideoName = CString(argv[1]);
				csFirstFileName = CString(argv[2]);
				csLastFileName = CString(argv[3]);

				int DirNumFirst = 0;
				int DirNumLast = 0;
				int FileNumFirst = 0;
				int FileNumLast = 0;
				CString csFinderStringFormat;

				CString DestParentDir;
				CString DestChildSuffix;
				CString DestFilePrefix;
				CString DestFileExt;
				int DestFileNumDigits;
				SplitImagePath(csFirstFileName, DestParentDir, DirNumFirst, DestChildSuffix, DestFilePrefix, DestFileNumDigits, FileNumFirst, DestFileExt);
				csFinderStringFormat.Format(_T("%s%%03d%s\\%s*.%s"), DestParentDir.GetString(), DestChildSuffix.GetString(), DestFilePrefix.GetString(), DestFileExt.GetString());
				SplitImagePath(csLastFileName, DestParentDir, DirNumLast, DestChildSuffix, DestFilePrefix, DestFileNumDigits, FileNumLast, DestFileExt);

				std::vector<CString> SourceImageList;
				int DirNum = DirNumFirst;
				int FileNum = FileNumFirst;
				do 
				{
					CString csFinderString;
					csFinderString.Format(csFinderStringFormat, DirNum);
					CFileFind finder;
					BOOL bWorking = finder.FindFile(csFinderString.GetString());
					while (bWorking)
					{
						bWorking = finder.FindNextFile();
						SplitImagePath(finder.GetFilePath(), DestParentDir, DirNum, DestChildSuffix, DestFilePrefix, DestFileNumDigits, FileNum, DestFileExt);
						if ((FileNum >= FileNumFirst) && (FileNum <= FileNumLast))
							SourceImageList.push_back(finder.GetFilePath());
					}
					finder.Close();
					DirNum++;
				} while (DirNum <= DirNumLast);

				std::wcout << "[" << getTimeISO8601() << "] " << "First File: " << csFirstFileName.GetString() << std::endl;
				std::wcout << "[" << getTimeISO8601() << "] " << "Last File:  " << csLastFileName.GetString() << std::endl;
				std::wcout << "[" << getTimeISO8601() << "] " << "Total Files: " << SourceImageList.size() << std::endl;

				TCHAR szPath[MAX_PATH] = _T("");
				SHGetFolderPath(NULL, CSIDL_MYVIDEO, NULL, 0, szPath);
				PathAddBackslash(szPath);
				CString csImageDirectory(szPath);
				csImageDirectory.Append(csVideoName);
				if (CreateDirectory(csImageDirectory, NULL))
				{
					int OutFileIndex = 0;
					for (auto SourceFile = SourceImageList.begin(); SourceFile != SourceImageList.end(); SourceFile++)
					{
						CString OutFilePath(csImageDirectory);
						OutFilePath.AppendFormat(_T("\\Wim%05d.JPG"), OutFileIndex++);
						std::wcout << "[" << getTimeISO8601() << "] " << "CopyFile " << SourceFile->GetString() << " to " << OutFilePath.GetString() << "\r";
						CopyFile(SourceFile->GetString(), OutFilePath, TRUE);
					}
					std::wcout << "\n";

					CString csImagePathSpec(csImageDirectory); csImagePathSpec.Append(_T("\\Wim%05d.JPG"));
					CString csVideoFullPath(csImageDirectory); csVideoFullPath.Append(_T(".mp4"));
					if (csFFMPEGPath.GetLength() > 0)
					{
						csVideoFullPath = csImageDirectory + _T("-1080p-cropped.mp4");
						std::wcout << "[" << getTimeISO8601() << "] " << csFFMPEGPath.GetString() << " -i " << QuoteFileName(csImagePathSpec).GetString() << " -y " << QuoteFileName(csVideoFullPath).GetString() << std::endl;
						if (-1 == _tspawnlp(_P_WAIT, csFFMPEGPath.GetString(), csFFMPEGPath.GetString(), 
							#ifdef _DEBUG
							_T("-report"),
							#endif
							_T("-i"), QuoteFileName(csImagePathSpec).GetString(),
							_T("-vf"), _T("crop=in_w:3/4*in_h"),
							// _T("-vf"), _T("rotate=PI"), // Us this to rotate the movie if we forgot to put the GoPro in upside down mode.
							_T("-s"), _T("1920x1080"),
							_T("-y"), // Cause it to overwrite exiting output files
							QuoteFileName(csVideoFullPath).GetString(), NULL))
							std::wcout << "[" << getTimeISO8601() << "]  _tspawnlp failed: " /* << _sys_errlist[errno] */ << std::endl;
						csVideoFullPath = csImageDirectory + _T("-1080p-compressed.mp4");
						std::wcout << "[" << getTimeISO8601() << "] " << csFFMPEGPath.GetString() << " -i " << QuoteFileName(csImagePathSpec).GetString() << " -y " << QuoteFileName(csVideoFullPath).GetString() << std::endl;
						if (-1 == _tspawnlp(_P_WAIT, csFFMPEGPath.GetString(), csFFMPEGPath.GetString(), 
							#ifdef _DEBUG
							_T("-report"),
							#endif
							_T("-i"), QuoteFileName(csImagePathSpec).GetString(),
							// _T("-vf"), _T("rotate=PI"), // Us this to rotate the movie if we forgot to put the GoPro in upside down mode.
							_T("-s"), _T("1920x1080"),
							_T("-y"), // Cause it to overwrite exiting output files
							QuoteFileName(csVideoFullPath).GetString(), NULL))
							std::wcout << "[" << getTimeISO8601() << "]  _tspawnlp failed: " /* << _sys_errlist[errno] */ << std::endl;
					}
					do 
					{
						CString OutFilePath(csImageDirectory);
						OutFilePath.AppendFormat(_T("\\Wim%05d.JPG"), --OutFileIndex);
						std::wcout << "[" << getTimeISO8601() << "] " << "DeleteFile " << OutFilePath.GetString() << "\r";
						DeleteFile(OutFilePath);
					} while (OutFileIndex > 0);
					std::wcout << "\n[" << getTimeISO8601() << "] " << "RemoveDirectory " << csImageDirectory.GetString() << std::endl;
					RemoveDirectory(csImageDirectory);
				}
			}
		}
	}
	else
	{
		_tprintf(_T("Fatal Error: GetModuleHandle failed\n"));
		nRetCode = 1;
	}
	return nRetCode;
}

Old Humor: Jesus & Satan Programming Contest

I’m cleaning up old text files on my machine and came across this story..

Jesus and Satan were having an ongoing argument about who was better on the computer. They had been going at it for days, and frankly God was tired of hearing all the bickering.

Finally fed up, God said, “THAT’s IT! I have had enough and I am going to set up a test that will run for two hours, and from those results, I will judge who does the better job.”

So Satan and Jesus sat down at the keyboards and typed away.
They moused.
They faxed.
They e-mailed
They e-mailed with attachments
They downloaded.
They did spreadsheets.
They wrote reports.
They created labels and cards.
They created charts and graphs.
They did some genealogy reports.
They did every job known to man.

Jesus worked with heavenly efficiency and Satan was faster then hell! Then, ten minutes before their time was up, lightning suddenly flashed across the sky, thunder rolled, rain poured and, of course, the power went off!

Satan stared at his blank screen and screamed every curse word known in the underworld. Jesus just sighed!

Finally the electricity came back on, and each of them restarted. Satan searching frantically, screaming……. “It’s gone! It’s ALL GONE! “I lost everything when the power went out!

Meanwhile, Jesus quietly started printing out all of his files from the past two hours of work. Satan observed this and became irate. “Wait!” he screamed…. “That’s not fair! He cheated! How come he has all his work and I don’t have any?”

God just shrugged and said, “Jesus Saves.”

I don’t know the origin. If you can legitimately claim it, I’ll definitely add an attribution.

UAV and GPS Dependency

Last weekend I was attempting to have my UAV fly a couple of programmed missions so that I could get a straightforward video of what I was doing.

My mission was simple, and I’d run similar missions before so I expected this to produce a nice video. I would take off, fly a circle with a 50 meter radius at 100 meters altitude, move to another location and fly a second circle at 30 meters altitude, then return to launch. Here’s what it looked like in Mission Planner.

Things look good in Mission Planner

Things look good in Mission Planner

Things started well, but went haywire a few minutes into the flight. There were a few bobbles as is descended from the first circle altitude to the second circle, but I was hoping to let the on-board computer maintain or recover control.


The flight starts around 1:40 on the video, and things generally look good until around 4:50. At that point the UAV starts making some significant attitude adjustments. Around 5:10 it seems to have recovered and making the adjustments to go around the second circle. (At 5:35 the camera is pointed back towards the launch point and you can see two small dots on the ground. That’s the two of us monitoring the flight with the transmitter and a pair of binoculars.) It finishes the second circle around 6:30, at which it should have initiated the RTL, Return to Launch, phase of its mission. Unfortunately I do not currently have the UAV set to face forward during the RTL phase, and so it does not rotate towards the launch point, just travels in that direction. You can see it tilt slightly back, moving in the correct direction, for the first ten seconds until 6:42, but then it starts making huge adjustments to its direction, running the motors at full throttle. At 8:08 it hit the trees.
After the flight I downloaded the log file from the UAV and tried to analyse what went wrong. I was able to produce this picture using Google Earth from the resulting KMZ file.

Mission Gone Bad

Mission Gone Bad

I’m still not sure exactly what went wrong. I believe that the copter lost resolution on the GPS, simply based on the fact that the track never shows it going to the trees, and I don’t remember it doing as much spiraling as the KMZ would indicate. The KMZ file may be available at my home server but right now it does not appear to want to serve kmz or kml files.  The descent from the first circle towards the second circle appears smooth until it no longer needed to travel horizontally, At the point when it needed to only reduce altitude it seemed to circle and appeared to be much more unstable. Perhaps making sure that all programmed descents had much more vertical motion would produce smoother flights in general.

A few valuable lessons that I learned, in no particular order.

  • Stick to flying where there are no people nearby. Even at an RC airplane field with a few experienced people around, this could cause significant injury to humans or damage to property. When the copter was coming in fast towards the landing area, and obviously going to overshoot into the trees, completely losing the copter in the trees is much better than causing any injury or damage to someone else.
  • Be quicker to abort an automated mission and take manual control. I had handed my transmitter to a more experienced pilot while I monitored the automatic mission via binoculars. Several times he asked if he should take control of the unit and I told him that I wanted to see if the unit would recover itself. The early bobbles during the descent from the first circle to the second it recovered, but the RTL path was completely wrong.
  • Give the UAV plenty of time to achieve a reliable GPS lock before arming the motors and taking off.  According to what I’ve read, the launch location used for the RTL function is stored when you arm the motors, not when you actually leave the ground, or power on the entire copter. Landing and disarming the motors in a second location would reset the launch location when you re-arm them.

An interesting thing I’ve noticed by following the drone chatter on the hype meter is how much of a clash this is causing between the computer and robotics crowd and the traditional RC airplane crowd.

The traditional RC airplane hobbyists have been around for over 50 years, and have slowly been integrating new technology such has digital radios, electric motors, and Lithium Polymer batteries into use as the technology as become available. Historical fixed wing aircraft and analog transmitters required large open spaces for both safety and to make sure that there was no radio interference. Liquid fueled model aircraft produced large amounts of noise, and so added to the reasons of working in large open spaces.

The person coming into the UAV/Drone space often has no background in the RC airplane hobby itself and so has no history of the safety requirements and why they exist. Starting with a quadcopter and its capabilities is completely different from starting with a fixed wing aircraft. VTOL, vertical takeoff and landing, brings a confidence that this can be done in a small space, however misplaced that confidence may be. First person video (FPV) seems a natural extension to most new pilots and the concept of limited bandwidth is completely alien.

There are good organizations such as http://www.modelaircraft.org/ that exist for hobbyists, but the person getting into it from the computer side of things often may not even know about them, and an unattractive web site may cause lack of understanding of the benefits offered.

I would not have gotten into this at all if a friend that I’ve known since we were in 4th grade had not added multi rotor copters to his long standing hobby of RC Airplanes. He was telling me how much was available in the new systems, and showing me first person videos on his screen. The fact that he was using all of these digital systems, but his video was still analog, was what got me interested. I wanted to be able to record the entire flight digitally.

I’ve been extremely lucky so far, not having damaged anything or injured anyone, and not broken more than $30 worth of items on my ‘copter so far, let alone lost it entirely. I’ve still got plenty to learn, as well as learning to be much safer, but I’m enjoying the process so far.

Logitech C920 Angle of View

I realized today that the Logitech C920 webcam produces images covering a different field of vision (FOV) for the same width based on the height. I was expecting the horizontal field to be the same for a given width but it was not.

Using the command ffmpeg -f video4linux2 -list_formats all -i /dev/video0 to retrieve the sizes of video available lists the same set of sizes for h264 and mjpeg. 640×480 160×90 160×120 176×144 320×180 320×240 352×288 432×240 640×360 800×448 800×600 864×480 960×720 1024×576 1280×720 1600×896 1920×1080. In Raw/yuyv422 mode two additional sizes are available. 2304×1296 2304×1536.

I pointed my webcam at the building out my window, giving myself a rough grid pattern to look at and ran it through all of the h.264 sizes, and manually counted the horizontal and vertical blocks visible. 

I expected 640×480 and 640×360 to be the same horizontal FOV but have different vertical FOV. What actually happened in the FOV was that they displayed the same vertical FOV but different horizontal FOV.

I ran through all of the h264 resolutions, and the vertical FOV appeared to shrink slightly when I requested resolutions below 200, but otherwise stayed the same. 

Selecting 2304×1536 produced a slightly larger vertical FOV with the same horizontal FOV as 1920×1080. 2304×1296 seemed to produce the same FOV in both directions as 1920×1080.  Both of these resolutions run at lower frame rates and only in raw mode. I was testing them using ffmpeg transcoding and sending to my windows desktop with the command: ffmpeg -re -f v4l2 -video_size 2304×1536 -framerate 2 -input_format yuyv422 -i /dev/video0 -f mpegts udp://192.168.0.10:8090

The C920 advertises a Diagonal FOV of 78°, but I didn’t find official meaning of that.  I found a nice bit of information at http://therandomlab.blogspot.com/2013/03/logitech-c920-and-c910-fields-of-view.html that describes it as explicitly as being when the camera is running in 16×9 mode. 

I will probably get around to writing a program to more accurately produce the results.  Here’s my manual table:

Resolution Width Height Blocks Floors Width/Height Ratio MegaPixels
160×90  160 90 9 8 1.777778 0.01
160×120  160 120 7 8 1.333333 0.01
176×144  176 144 7 9 1.222222 0.02
320×180  320 180 9 8 1.777778 0.05
320×240  320 240 7 9 1.333333 0.07
352×288  352 288 7 9 1.222222 0.1
432×240  432 240 10 9 1.8 0.1
640×360  640 360 10 9 1.777778 0.23
640×480  640 480 7 9 1.333333 0.3
800×448  800 448 10 9 1.785714 0.35
800×600  800 600 7 9 1.333333 0.48
864×480  864 480 10 9 1.8 0.41
960×720  960 720 7 9 1.333333 0.69
1024×576  1024 576 10 9 1.777778 0.58
1280×720  1280 720 10 9 1.777778 0.92
1600×896  1600 896 10 9 1.785714 1.43
1920×1080 1920 1080 10 9 1.777778 2.07
2304×1296 2304 1296     1.777778 2.98
2304×1536 2304 1536     1.5 3.53
  16 9     1.777778
  4 3     1.333333

 

BeagleBoneBlack 5.8GHz WiFi Reliability

After upgrading the operating system, providing more power via a powered USB Hub, and better understanding the startup scripts, I seem to have a reliable WiFi link from my BBB.

I still have occasional problems at boot time with the device not connecting to my WiFi network. I’ve got an FTDI USB-SerialTTL console cable that I can connect to the device and examine the status. Most of the time when I’ve not been able to reach the device over the network and I do this, running the lsusb command produces results showing nothing connected beyond the internal USB devices.

root@beaglebone:~# lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

If I disconnect the USB hub, remove and reapply it’s power, and reconnect the USB hub, sometimes it will cause the BBB to recognize the USB devices, but often it requires removing all power, disconnecting the hub, and reconnecting everything.

USB Power is the first issue in getting things to work. I only have the verbose reports from the lsusb command to go on for deciding how much power I need. The spec sheet for the BBB reports that it can only supply 500 mA on it’s USB port, and even then only if it’s powered by an external power adapter via the barrel jack. My WiFi adapter reports 450 mA. My camera reports 500mA. The hub in self powered operation reports 100mA. The power adapter that came with my hub reports it’s output as 2.1A, which would indicate that it should be able to provide the standard 500mA to each of it’s 4 ports if it’s running on external power.

root@beaglebone:~# lsusb ; lsusb --verbose | grep MaxPower
Bus 001 Device 002: ID 0409:005a NEC Corp. HighSpeed Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 13b1:002f Linksys AE1000 v1 802.11n [Ralink RT3572]
Bus 001 Device 004: ID 046d:082d Logitech, Inc.
    MaxPower              100mA
    MaxPower                0mA
    MaxPower                0mA
    MaxPower              450mA
    MaxPower              500mA

I’m running a system that I started by flashing my eMMC with the 9/4/2013 image I downloaded from http://circuitco.com/support/index.php?title=Updating_The_Software#Procedure

The dmesg command reports the kernel as “Linux version 3.8.13 (koen@rrMBP) (gcc version 4.7.3 20130205 (prerelease) (Linaro GCC 4.7-2013.02-01) ) #1 SMP Wed Sep 4 09:09:32 CEST 2013”

I am running with a 32GB micro sd card installed, and partitioned into two volumes. In the root of the FAT volume I’ve got a uEnv.txt file that continues the boot process to the eMMC and it also issues the kernel command to disable the internal HDMI cape on the BBB. Since I’m only running this device over the network, I have decided it is more efficient to disable the HDMI entirely. I don’t think that the HDMI changes affect my WiFi, but I’ve not investigated it either.

root@beaglebone:~# fdisk -l /dev/mmcblk0 /dev/mmcblk1

Disk /dev/mmcblk0: 31.9 GB, 31914983424 bytes, 62333952 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x00000000

        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1            2048    41945087    20971520    c  W95 FAT32 (LBA)
/dev/mmcblk0p2        41945088    62333951    10194432   83  Linux

Disk /dev/mmcblk1: 1920 MB, 1920991232 bytes, 3751936 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x00000000

        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk1p1   *          63      144584       72261    c  W95 FAT32 (LBA)
/dev/mmcblk1p2          144585     3743144     1799280   83  Linux

root@beaglebone:~# cat /media/BONEBOOT/uEnv.txt
mmcdev=1
bootpart=1:2
mmcroot=/dev/mmcblk1p2
optargs=quiet capemgr.disable_partno=BB-BONELT-HDMI,BB-BONELT-HDMIN

root@beaglebone:~# cat /etc/fstab
rootfs               /                    auto       defaults              1  1
proc                 /proc                proc       defaults              0  0
devpts               /dev/pts             devpts     mode=0620,gid=5       0  0
tmpfs                /tmp                 tmpfs      defaults              0  0
/dev/mmcblk0p2       /home                auto       defaults              0  2
/dev/mmcblk0p1       /media/BONEBOOT      auto       defaults              0  2
/dev/sda1            /media/PNY           auto       noauto                0  2
/dev/mmcblk1p1       /media/BEAGLEBONE    auto       ro                    0  2

I have created a file /var/lib/connman/wifi.config that has two sections, one for each of the wifi networks that I regularly connect to. The first is my primary network, and it seems to be stable connecting. The second is a network I occasionally power up, but I’ve not spent much time testing it. The good thing is that the credentials are in one place, and it’s supposed to chose the first network in the list that is found.

root@beaglebone:~# cat /var/lib/connman/wifi.config
[service_WimsWorld-5G]
Type = wifi
Name = WimsWorld-5G
Security = wpa2-psk
Passphrase = MyPasswordInPlainText

[service_WimsWorld-UAV]
Type = wifi
Name = WimsWorld-UAV
Security = wpa2-psk
Passphrase = MyPasswordInPlainText

I created /etc/udev/rules.d/70-wifi-powersave.rules following the information in https://wiki.archlinux.org/index.php/Power_saving#Network_interfaces , paying explicit attention to the fact that naming the file matters.

In this case, the name of the configuration file is important. Due to the introduction of persistent device names via 80-net-name-slot.rules in systemd v197, it is important that the network powersave rules are named lexicographically before 80-net-name-slot.rules, so that they are applied before the devices are named e.g. enp2s0.

root@beaglebone:~# cat /etc/udev/rules.d/70-wifi-powersave.rules
ACTION=="add", SUBSYSTEM=="net", KERNEL=="wlan*", RUN+="/usr/sbin/iw dev %k set power_save off"

The iw dev wlan0 set power_save off command disables a WiFi feature called power save mode. I believe it is part of the 802.11 standard, but support varies by driver and chipset. It gets negotiated between the client device and the access point on authentication. If it is enabled, the access point may buffer multiple small packets before sending them to the client and the client spends less time either transmitting or receiving. If I run the command ping -t 192.168.0.17 from my windows machine with power_save off, the time is very stable at 1 to 2ms. If I get a connection with power_save on, the time varies greatly with most times reported over 100ms.

My home network has plenty of nearby networks to conflict with.

root@beaglebone:~# iw wlan0 scan | grep SSID | sort
        SSID: Aman-Guest
        SSID: Aman2.4G
        SSID: Aman5G
        SSID: Angela's Wi-Fi Network
        SSID: Battlestar Galactica
        SSID: Battlestar Galactica
        SSID: CenturyLink0705
        SSID: Cyberia
        SSID: Dagobah
        SSID: Derek's Wi-Fi Network
        SSID: HP-Print-60-LaserJet 100
        SSID: HSE-1305(a) .media
        SSID: Jaggernet
        SSID: Jaggernett
        SSID: Joergstrasse
        SSID: Joergstrasse5
        SSID: Joshernet
        SSID: MOTOROLA-06F23
        SSID: NCH1205
        SSID: NCH515
        SSID: NCH611
        SSID: NETGEAR84
        SSID: Paris
        SSID: PhishingNet
        SSID: Poop2 5GHz
        SSID: PoopTime
        SSID: SMC
        SSID: Se1301
        SSID: Seattle2GHz
        SSID: SusansWIFI
        SSID: WimsWorld
        SSID: WimsWorld-5G
        SSID: XVI
        SSID: bedford
        SSID: bedford
        SSID: go-seahawks
        SSID: goodtimes
        SSID: goodtimes-guest
        SSID: ladines
        SSID: maverick
        SSID: mridula_air
        SSID: shubaloo
        SSID: shubaloo-5g
        SSID: washington

One other change that I made was to disable the cpu-ondemand.timer service with the command:

systemctl disable cpu-ondemand.timer

I don’t know if that has affected my WiFi stability, but it has certainly made my overall system more stable. By default this service runs after the BBB has been running for ten minutes, and then puts the system clock into variable mode with the command cpufreq-set -g ondemand. I ran into problems with my machine changing it’s internal frequency on a regular basis. for my purposes, I chose to leave the CPU in it’s default state, running with the performance governor, which leaves it at 1000 MHz. run the command cpufreq-info to see what state the BBB is currently in, and what it’s possible to change it to.

My machine seems to be stable right now, as can be shown by nothing being added to the dmesg log since the initial boot, 19 and a half hours ago.

root@beaglebone:~# dmesg | tail -32 ; uptime
[    9.360135] usb0: eth_open
[    9.360359] IPv6: ADDRCONF(NETDEV_UP): usb0: link is not ready
[   10.281944] gs_open: ttyGS0 (dcaccc00,dcaa8600)
[   10.282105] gs_close: ttyGS0 (dcaccc00,dcaa8600) ...
[   10.282119] gs_close: ttyGS0 (dcaccc00,dcaa8600) done!
[   10.283944] gs_open: ttyGS0 (dcaccc00,dcd1f980)
[   11.637465] usb0: stop stats: rx/tx 0/0, errs 0/0
[   11.742846] ip_tables: (C) 2000-2006 Netfilter Core Team
[   12.058808] net eth0: initializing cpsw version 1.12 (0)
[   12.070772] net eth0: phy found : id is : 0x7c0f1
[   12.070810] libphy: PHY 4a101000.mdio:01 not found
[   12.075883] net eth0: phy 4a101000.mdio:01 not found on slave 1
[   12.133068] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready
[   12.694713] IPv6: ADDRCONF(NETDEV_UP): wlan0: link is not ready
[   18.301568] wlan0: authenticate with 20:4e:7f:85:ce:5b
[   18.327171] wlan0: send auth to 20:4e:7f:85:ce:5b (try 1/3)
[   18.327734] wlan0: authenticated
[   18.336184] wlan0: associate with 20:4e:7f:85:ce:5b (try 1/3)
[   18.337359] wlan0: RX AssocResp from 20:4e:7f:85:ce:5b (capab=0x411 status=0 aid=2)
[   18.342420] wlan0: associated
[   18.342545] IPv6: ADDRCONF(NETDEV_CHANGE): wlan0: link becomes ready
[   18.342777] cfg80211: Calling CRDA for country: US
[   18.342940] cfg80211: Regulatory domain changed to country: US
[   18.342951] cfg80211:   (start_freq - end_freq @ bandwidth), (max_antenna_gain, max_eirp)
[   18.342962] cfg80211:   (2402000 KHz - 2472000 KHz @ 40000 KHz), (300 mBi, 2700 mBm)
[   18.342973] cfg80211:   (5170000 KHz - 5250000 KHz @ 40000 KHz), (300 mBi, 1700 mBm)
[   18.342983] cfg80211:   (5250000 KHz - 5330000 KHz @ 40000 KHz), (300 mBi, 2000 mBm)
[   18.342993] cfg80211:   (5490000 KHz - 5600000 KHz @ 40000 KHz), (300 mBi, 2000 mBm)
[   18.343003] cfg80211:   (5650000 KHz - 5710000 KHz @ 40000 KHz), (300 mBi, 2000 mBm)
[   18.343013] cfg80211:   (5735000 KHz - 5835000 KHz @ 40000 KHz), (300 mBi, 3000 mBm)
[   18.343022] cfg80211:   (57240000 KHz - 63720000 KHz @ 2160000 KHz), (N/A, 4000 mBm)
[   18.418237] wlan0: Limiting TX power to 23 (23 - 0) dBm as advertised by 20:4e:7f:85:ce:5b
 16:34:09 up 19:35,  1 user,  load average: 0.03, 0.07, 0.05

BeagleBoneBlack Webcam using a V4L2, FFMPEG and a FIFO

In my previous post I was starting FFMPEG with the command to read from standard input using a pipe. That’s a great method when you have a single input you want to supply to FFMPEG, but my ultimate goal is to supply multiple streams to FFMPEG and have it multiplex them into a single transport stream.

I believe that my end solution for this is going to be using several named pipes, which are First In First Out devices created by the command mkfifo() in linux.

The following code snippet functions nearly identically to the snippet in my previous post except that it creates a FIFO in the /tmp/ directory and tells FFMPEG to read from the FIFO instead of from stdin.

if (0 != mkfifo("/tmp/Wim_C920_FFMPEG", S_IRWXU))
	std::cerr << "[" << getTimeISO8601() << "] FiFo /tmp/Wim_C920_FFMPEG NOT created Successfully" << std::endl;
else
	std::cerr << "[" << getTimeISO8601() << "] FiFo /tmp/Wim_C920_FFMPEG created Successfully" << std::endl;
/* Attempt to fork */
pid_t pid = fork();
if(pid == -1)
{
	fprintf(stderr,"Fork error! Exiting.\n");  /* something went wrong */
	exit(1);        
}
else if (pid == 0)
{
	/* A zero PID indicates that this is the child process */
	/* Replace the child fork with a new process */
	if(execlp("ffmpeg", "ffmpeg", "-i", "/tmp/Wim_C920_FFMPEG", "-vcodec", "copy", "-f", "rtp", "rtp://239.8.8.8:8090/", "-metadata", "title=\"Wims BoneCam\"", "-vcodec", "copy", OutputFilename.c_str(), NULL) == -1)
	{
		std::cerr << "execlp Error! Exiting." << std::endl;
		exit(1);
	}
}
else
{
	/* A positive (non-negative) PID indicates the parent process */
	open_device();
	init_device();
	start_capturing();
	//mainloop();
	int pipe_fd = open("/tmp/Wim_C920_FFMPEG", O_WRONLY);
	if (pipe_fd <= 0)
		std::cerr << "[" << getTimeISO8601() << "] FiFo NOT opened Successfully" << std::endl;
	else
	{
		std::cerr << "[" << getTimeISO8601() << "] FiFo opened Successfully" << std::endl;
		time_t CurrentTime;
		time(&CurrentTime);
		time_t FileStartTime = CurrentTime;
		while ((difftime(CurrentTime, FileStartTime) < (60 * 60)) && (bRun))
		{
			fd_set fds;
			struct timeval tv;
			int r;

			FD_ZERO(&fds);
			FD_SET(fd, &fds);

			/* Timeout. */
			tv.tv_sec = 2;
			tv.tv_usec = 0;

			r = select(fd + 1, &fds, NULL, NULL, &tv);
			if (-1 == r) 
			{
				if (EINTR == errno)
					continue;
				errno_exit("select");
			}
			if (0 == r) 
			{
				fprintf(stderr, "select timeout\n");
				exit(EXIT_FAILURE);
			}
			struct v4l2_buffer buf;
			WIMLABEL:
			CLEAR(buf);
			buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
			buf.memory = V4L2_MEMORY_MMAP;

			if (-1 == xioctl(fd, VIDIOC_DQBUF, &buf)) 
			{
				switch (errno) 
				{
				case EAGAIN:
					goto WIMLABEL;
				case EIO:
					/* Could ignore EIO, see spec. */
					/* fall through */
				default:
					errno_exit("VIDIOC_DQBUF");
				}
			}
			else
			{
				assert(buf.index < n_buffers);
				write(pipe_fd, buffers[buf.index].start, buf.bytesused);
			}
			if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))
					errno_exit("VIDIOC_QBUF");
			time(&CurrentTime);
		}
		stop_capturing();
		uninit_device();
		close_device();
		close(pipe_fd);		/* Close side of pipe I'm writing to, to get the child to recognize it's gone away */
		std::cerr << "\n[" << getTimeISO8601() << "] Pipe Closed, Waiting for FFMPEG to exit" << std::endl;
	}
	int ffmpeg_exit_status;
	wait(&ffmpeg_exit_status);				/* Wait for child process to end */
	std::cerr << "[" << getTimeISO8601() << "] FFMPEG exited with a  " << ffmpeg_exit_status << " value" << std::endl;
	if (0 == remove("/tmp/Wim_C920_FFMPEG"))
		std::cerr << "[" << getTimeISO8601() << "] FiFo /tmp/Wim_C920_FFMPEG removed Successfully" << std::endl;
	else
		std::cerr << "[" << getTimeISO8601() << "] FiFo /tmp/Wim_C920_FFMPEG NOT removed Successfully" << std::endl;
}

An interesting side effect is that ffmpeg now can read from stdin for arbitrary commands, as if it had been run from the linux command line, and so the "q" key will cause it to quit running. I've not fully investigated this behavior to see what happens to the parent program when the fifo consumer suddenly quits. Initially, it seems that the fifo supplier quits as well.

A bad thing about going with named pipes is that there should be a way of ensuring that the names used are unique and don't conflict with anything else on the system. They should also be cleaned up at the end of the process. My current method has very little error correction and should not be used as an example beyond to get started.