Working with the IoT and Windows 10
So I decided a long time ago to use the Raspberry Pi 2 for a little home experiment regarding humidity in my crawlspaces and basement. It's already costing me thousands, so putting $50 into it wasn't that painful. It got parked a while ago, but the problem is back.
I'm writing this post to remember the fine points of how I did it, so I can do it again when the time comes. I'm using the following tools:
- Visual Studio 2015, Enterprise Edition
- C# as the language
- A Raspberry Pi 2 B (there is already a 3, out)
- Windows 10 IoT (Internet of Things)
- Compatible WiFi dongle
- 32gb SanDisk chip for the disk
- WeatherShield from Sparkfun
Start by flashing the card with the latest operating system for the RPI - in my case that was 10.0.10586. Also, using the IoT Dashboard that comes with your utilities when you install the Windows IoT tools, change the computer name and the password. You can use the web interface to do that, too. Either way, doing it now will save you repeating some steps later.
Programming was straightforward. There are tons of examples on the net. You have to watch your back with Async and Await as it itends to bubble it's way into everything and can create problems with timers and so on.
The real surprise was that Table Storage doesn't take decimal or float data types. >.< All of the sensors put out floats. OK. So I converted them to doubles, and rounded them to two places so that data wasn't so obnoxious to read.
Humidity = Math.Round( System.Convert.ToDouble( sd.Humidity ) , 2 , MidpointRounding.AwayFromZero ) ,
In another surprise, you have to read the entity from storage before you try to update it. That's to make sure you get the correct timestamp and you don't have collisions.
private async Task SetLocation() {
location = await GetLocation();
if ( location == null )
{
await CreateLocationEntry();
}
else
{
location.LastStarted = DateTime.UtcNow;
TableOperation updateOp = TableOperation.Replace( location );
await locationsTable.ExecuteAsync( updateOp );
}
}
private async Task<LocationEntity> GetLocation() {
var table = tableClient.GetTableReference( Constants.LocationTableName );
await table.CreateIfNotExistsAsync();
TableOperation retrieveOp = TableOperation.Retrieve<LocationEntity>( deviceName , deviceId );
TableResult retrieveOpResult = await table.ExecuteAsync( retrieveOp );
return ( LocationEntity )retrieveOpResult.Result;
}
private async Task CreateLocationEntry() {
var timeStamp = DateTime.UtcNow;
var loc = new LocationEntity( deviceName , deviceId ) {
CreatedOn = timeStamp ,
LastStarted = timeStamp ,
ReadingInterval = Constants.DefaultReadingFrequencyMs ,
};
TableOperation insertOp = TableOperation.Insert( loc );
TableResult insertResult = await locationsTable.ExecuteAsync( insertOp );
location = ( LocationEntity )insertResult.Result;
}
You have to create a taskDeferral
that you can get from the taskInstance
that is passed into the Run
method.
You also have to handle the cancellation event
in the task instance create at startup:
public async void Run( IBackgroundTaskInstance taskInstance ) {
// handle the cancellation event
taskInstance.Canceled += new BackgroundTaskCanceledEventHandler( OnCanceled );
// keep our background task afloat
taskDeferral = taskInstance.GetDeferral();
.
.
.
i2cTimer = ThreadPoolTimer.CreatePeriodicTimer(
GetReadingsAtIntverals ,
TimeSpan.FromMilliseconds( location.ReadingInterval ) );
}
private void OnCanceled( IBackgroundTaskInstance sender , BackgroundTaskCancellationReason reason ) {
if ( _CancelRequested )
{
i2cTimer.Cancel();
taskDeferral.Complete();
}
}
Deployment
is where I had the most trouble. There's a lot of options about how to distribute your app and most of them didn't apply to me. Lost a whole day figuring this out.
After you've debugged your app, set the target environment to ARM
(because this is running on a RPI) and your mode to Release
which will eliminate all the debugging code.
Then Clean
the solution and Rebuild
it. Use the project property page to set the target to Remote
. At this point you'll have to know the IP address of the RPI. Then use Ctrl+F5
to run it without debugging. The status bar will tell you it's building the app and then it will move on to deploying the app. When that's finished you can move on to the next step. It might take a few so be patient.
I never did get the web page RPI management system to select the default app or start my app to actually work. So I punted and went back to command lines.
I used the steps from this excellent Microsoft document, and this great Microsoft Hello World sample but those often get moved and lost. Here's a deployment summary in case that one is missing:
- After you rename your RPI and change the password....
- Launch Powershell as an administrator on your PC
- Type
net start winrm
- This gives you a remote console service. - Type
Set-Item WSMan:\localhost\Client\TrustedHosts -Value <RPI IP Address>
- This cleans up some networking security issues. - Type
remove-module psreadline -force
- This prevents Stackoverflow condition that can happen sometimes. - Type
Enter-PSSession -ComputerName <IP Address> -Credential <IP Address>\Administrator
- This gives you a remote control console to the RPI. - Type
iotstartup list
to get a list of all apps. Some will be "headed" because they have a UI and some will be "headless" because they are just services and not user apps. It's odd that this command doesn't really list apps that are going to start up. Mr. Literal here took a while to realize that was the case. - You should see your app in there. Easiest thing to do is just copy the entire name of your app onto the clipboard. It will be long and ugly and you won't want to type it.
- Type
iotstartup add headless <your pasted app name here>
and press enter. - It will say "Added Headless
" and you shouldn't get any errors. - Reboot your RPI and you should see it doing whatever you planned for it. In my case, it was dumping data up to Azure Storage tables. Realize that you will lose your remote management and have to start at the
Enter-PsSession...
prompt again.
These might help: