Hacking Izon Cameras and using Azure IoT Edge

After Izon announced that they were closing down their services (leaving the cameras I already owned useless), I decided to turn them into something useful using Azure. First let me list some resources:

Use the Will it hack link to get access to the mobileye website and verify that the Izon device is still streaming and still working. If it is working, you are already done with edits to the device unless you would like to change the passwords (which you should).

Our goals are as follows:

  • Process the video feed from the Izon camera (we will cheat this early on and only use the image feed)
    • Check for motion
    • Check for faces
    • Check if faces are white listed
    • Check for my dog
  • Process the audio feed
    • Check for any noise
    • Check for non human noises
    • Check for dog barks
    • Check for my and my wife’s voice

These are all stretch goals that will be referred back to as the project moves forward.

Create the Azure IoT Edge module

For the first module, we will use the C Module base image. We are looking for two things from this module:

  • Download the picture feed and pass it to the Edge Hub
  • Download the audio feed and pass it to the Edge Hub

If you don’t know where to get started with the C module of the Azure IoT Edge platform, there is helpful information on the Azure Documentation page. Once the C module is created and ready for editing, we are going to connect to the image feed from the devices. To make this simple, both feeds will be retrieved using HTTP. For the video feed, its simple enough to grab images from the Izon camera existing camera feed.

Now one thing we need, is to be able to connect to each camera within the local network shared with the Edge. Since we would like to be able to add and remove cameras, we will use the device twin to update and manage the list of IP address. The code for updating the list is as follows:


#include <stdio.h>
#include <stdlib.h>
#include "iothub_module_client_ll.h"
#include "iothub_client_options.h"
#include "iothub_message.h"
#include "azure_c_shared_utility/threadapi.h"
#include "azure_c_shared_utility/crt_abstractions.h"
#include "azure_c_shared_utility/platform.h"
#include "azure_c_shared_utility/shared_util_options.h"
#include "iothubtransportmqtt.h"
#include "iothub.h"
#include "time.h"
#include "parson.h"
typedef struct IP_ADDRESS_NODE
{
const char * address;
struct IP_ADDRESS_NODE * next;
} ip_address_node;
ip_address_node * add_address(const char * address,ip_address_node * previous)
{
ip_address_node * new_node = (ip_address_node *)malloc(sizeof(ip_address_node));
new_node->address = address;
new_node->next = NULL;
if (previous == NULL)
{
return new_node;
}
previous->next = new_node;
return previous;
}
void delete_address(ip_address_node * current)
{
if (current == NULL)
{
return;
}
delete_address(current->next);
//free(current->address);
free(current);
}
ip_address_node * root_node = NULL;
ip_address_node * add_address_to_root(const char * address)
{
if (root_node = NULL)
{
root_node = add_address(address,root_node);
}
ip_address_node * current = root_node;
while(current->next != NULL)
{
current = current->next;
}
add_address(address,current);
}
static void moduleTwinCallback(DEVICE_TWIN_UPDATE_STATE update_state, const unsigned char* payLoad, size_t size, void* userContextCallback)
{
printf("\r\nTwin callback called with (state=%s, size=%zu):\r\n%s\r\n",
ENUM_TO_STRING(DEVICE_TWIN_UPDATE_STATE, update_state), size, payLoad);
JSON_Value *root_value = json_parse_string(payLoad);
JSON_Object *root_object = json_value_get_object(root_value);
JSON_Array * ipaddresses = json_object_dotget_array(root_object, "desired.CameraAddresses");
if (ipaddresses != NULL) {
delete_address(root_node);
for (int i = 0; i < json_array_get_count(ipaddresses); i++) {
add_address_to_root(json_array_get_string(ipaddresses,i));
}
return;
}
ipaddresses = json_object_get_array(root_object, "CameraAddresses");
if (ipaddresses != NULL) {
delete_address(root_node);
for (int i = 0; i < json_array_get_count(ipaddresses); i++) {
add_address_to_root(json_array_get_string(ipaddresses,i));
}
return;
}
}

view raw

main.c

hosted with ❤ by GitHub

With that code in place, the list of IP addresses can be updated from the Azure UI and the Azure Service SDKs.

Downloading from the Image feed

The Izon cameras make downloading the image feed trivial. There is an existing endpoint where you can grab the latest image directly from the camera’s web server. The latest image is always at /cgi-bin/img-d1.cgi. (NOTE: if you are checking this image from a browser, be sure to have some cache busting!). To download this image into our module, we will use the Curl library for it’s easy HTTP implementation. To add Curl to our Edge module, we will add the following lines to the Dockerfile.amd64.debug:


FROM ubuntu:xenial AS base
RUN apt-get update && \
apt-get install -y –no-install-recommends software-properties-common gdb && \
add-apt-repository -y ppa:aziotsdklinux/ppa-azureiot && \
apt-get update && \
apt-get install -y azure-iot-sdk-c-dev && \
rm -rf /var/lib/apt/lists/*
FROM base AS build-env
RUN apt-get update && \
apt-get install -y –no-install-recommends cmake gcc g++ make libcurl4-openssl-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . ./
RUN cmake -DCMAKE_BUILD_TYPE=Debug .
RUN make
FROM base
WORKDIR /app
COPY –from=build-env /app ./
CMD ["./main"]


FROM ubuntu:xenial AS base
RUN apt-get update && \
apt-get install -y –no-install-recommends software-properties-common gdb && \
add-apt-repository -y ppa:aziotsdklinux/ppa-azureiot && \
apt-get update && \
apt-get install -y azure-iot-sdk-c-dev && \
rm -rf /var/lib/apt/lists/*
FROM base AS build-env
RUN apt-get update && \
apt-get install -y –no-install-recommends cmake gcc g++ make && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . ./
RUN cmake -DCMAKE_BUILD_TYPE=Debug .
RUN make
FROM base
WORKDIR /app
COPY –from=build-env /app ./
CMD ["./main"]

With curl now added to the image, it can be utilized in code by adding it to the method invoked in our main loop. The code will download the file for each entry in the IP address list. Once the image is downloaded, it will send it as a message to the Edge Hub and add the IP address of the camera to the message header. Here is that code:


struct MemoryStruct {
char *memory;
size_t size;
};
static size_t
WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
if(ptr == NULL) {
/* out of memory! */
printf("not enough memory (realloc returned NULL)\n");
return 0;
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;
return realsize;
}
struct MemoryStruct download_file(const char * address)
{
struct MemoryStruct chunk;
chunk.memory = malloc(1); /* will be grown as needed by the realloc above */
chunk.size = 0; /* no data at this point */
/* init the curl session */
CURL * curl_handle = curl_easy_init();
/* specify URL to get */
curl_easy_setopt(curl_handle, CURLOPT_URL, address);
/* send all data to this function */
curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
/* we pass our 'chunk' struct to the callback function */
curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
/* some servers don't like requests that are made without a user-agent
field, so we provide one */
curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");
/* get it! */
CURLcode res = curl_easy_perform(curl_handle);
/* check for errors */
if(res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
}
/* cleanup curl stuff */
curl_easy_cleanup(curl_handle);
return chunk;
}
void download_image(IOTHUB_MODULE_CLIENT_LL_HANDLE iotHubModuleClientHandle)
{
ip_address_node * address_node = root_node;
do
{
struct MemoryStruct image = download_file(address_node->address);
if (image.size > 1)
{
IOTHUB_MESSAGE_HANDLE message_handle = IoTHubMessage_CreateFromByteArray((char*)image.memory, image.size);
MAP_HANDLE propMap = IoTHubMessage_Properties(message_handle);
Map_AddOrUpdate(propMap, "IpAddress", address_node->address);
IOTHUB_CLIENT_RESULT clientResult = IoTHubModuleClient_LL_SendEventToOutputAsync(iotHubModuleClientHandle, message_handle, "IncomingImage", NULL,NULL);
if (clientResult != IOTHUB_CLIENT_OK)
{
IoTHubMessage_Destroy(message_handle);
printf("IoTHubModuleClient_LL_SendEventToOutputAsync failed on sending to output IncomingImage, err=%d\n", clientResult);
}
}
free(image.memory);
address_node = address_node->next;
} while(address_node != NULL);
}

view raw

main.c

hosted with ❤ by GitHub

Downloading from the Audio feed

Now that the image feed is being published to the Edge Hub, its time to connect the audio feed. The audio feed is trickier since the Izon camera doesn’t have an easy to use endpoint (that I know of) for downloading the audio samples like we can with the image feed. In the next entry in this series, an Audio feed will be derived from an RSTP stream.

 

Using Angular Kendo Grid with Elastic Search and ASP.NET Core

There was a need for using a Kendo Grid in an Angular 5 website where the backing store for the data was Elastic Search. Utilizing the filtering on local data was simple enough but for the needs of filtering there needed to be server side integration. The server was running ASP.NET Core.

To get started create a view and view model for Angular to expose the grid.


import { Component } from '@angular/core';
import { process, State } from '@progress/kendo-data-query';
import { sampleProducts } from './products';
import {
GridComponent,
GridDataResult,
DataStateChangeEvent
} from '@progress/kendo-angular-grid';
@Component({
selector: 'my-app',
template: `
<kendo-grid
[data]="gridData"
[pageSize]="state.take"
[skip]="state.skip"
[sort]="state.sort"
[filter]="state.filter"
[sortable]="true"
[pageable]="true"
[filterable]="true"
(dataStateChange)="dataStateChange($event)">
<kendo-grid-column field="ProductID" title="ID" width="40" [filterable]="false">
</kendo-grid-column>
<kendo-grid-column field="ProductName" title="Product Name">
</kendo-grid-column>
<kendo-grid-column field="FirstOrderedOn" title="First Ordered On" width="240" filter="date" format="{0:d}">
</kendo-grid-column>
<kendo-grid-column field="UnitPrice" title="Unit Price" width="180" filter="numeric" format="{0:c}">
</kendo-grid-column>
<kendo-grid-column field="Discontinued" width="120" filter="boolean">
<ng-template kendoGridCellTemplate let-dataItem>
<input type="checkbox" [checked]="dataItem.Discontinued" disabled/>
</ng-template>
</kendo-grid-column>
</kendo-grid>
`
})
export class AppComponent {
public state: State = {
skip: 0,
take: 5,
// Initial filter descriptor
filter: {
logic: 'and',
filters: [{ field: 'ProductName', operator: 'contains', value: 'Chef' }]
}
};
public gridData: GridDataResult = process(sampleProducts, this.state);
public dataStateChange(state: DataStateChangeEvent): void {
this.state = state;
this.gridData = process(sampleProducts, this.state);
}
}


import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from '@angular/common/http';
import { GridModule } from '@progress/kendo-angular-grid';
import { AppComponent } from './app.component';
@NgModule({
bootstrap: [
AppComponent
],
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
GridModule
]
})
export class AppModule { }

view raw

app.module.ts

hosted with ❤ by GitHub


import { AppModule } from './app.module';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

view raw

main.ts

hosted with ❤ by GitHub

To wire the view and view model to the server side data, there needs to be an Angular HTTP service and an ASP.NET Core Controller. The controller needs to be able to accept the filter and paging options of the grid as the user changes them and react to them server side. To accomplish this, some objects need to be created to handle the request:

First, the filter object, which is changed whenever a new filter is selected or is cleared; must be mapped to a C# object that can be serialized. The structure of the Kendo Grid filter is as such:

filter: {
      logic: 'and',
      filters: [{ field: 'ProductName', operator: 'contains', value: 'Chef' }]
}

To make that object transportable to C#, lets create a POCO:


public class CompositeFilterDescriptor
{
[JsonProperty("logic")]
public string Logic { get; set; }
[JsonProperty("filters")]
public ICollection<CompositeFilterDescriptor> Filters { get; set; }
[JsonProperty("field")]
public string Field { get; set; }
[JsonProperty("operator")]
public string Operator { get; set; }
[JsonProperty("value")]
public object Value { get; set; }
[JsonProperty("ignoreCase")]
public bool? IgnoreCase { get; set; }
}


public class Page
{
public int Skip { get; set; }
public int Take { get; set; }
}

view raw

Page.cs

hosted with ❤ by GitHub


public class RequestDto
{
public CompositeFilterDescriptor Filters { get; set; }
public Page Page { get; set; }
}

view raw

RequestDto.cs

hosted with ❤ by GitHub

Now lets create an ASP.NET Core controller endpoint for our Filter.


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Nest;
public class GridController : Controller
{
private readonly ElasticClient _elasticClient;
public GridController()
{
//Initalize ElasticClient
}
[HttpPost]
public async Task<IActionResult> PostRequest([FromBody]RequestDto request)
{
CompositeFilterMapper compositeFilterMapper = new CompositeFilterMapper();
//Grab a page from elastic
selector = search =>
search.Index("your-index")
.From(request.Page.Skip)
.Size(request.Page.Take)
.Query(q =>
compositeFilterMapper.GetQueryContainer(q));
var searchResponse = _elasticClient.Search(selector);
return Ok(new ResponseDto
{
Results = searchResponse.Documents,
Count = searchResponse.Total
});
}
}


public class ResponseDto<T>
{
public IEnumerable<T> Results { get; set; }
public long Count { get; set; }
}

view raw

ResponseDto.cs

hosted with ❤ by GitHub

The only thing missing now is the query to work is the CompositeFilterMapper.


class CompositeFilterMapper<T>
{
private readonly CompositeFilterDescriptor _compositeFilterDescriptor;=
public CompositeFilterMapper(CompositeFilterDescriptor compositeFilterDescriptor)
{
_compositeFilterDescriptor = compositeFilterDescriptor;
}
public QueryContainer GetQueryContainer(QueryContainerDescriptor<ElasticDataUpload<T>> query)
{
return ApplyQuery(new[] {_compositeFilterDescriptor}, _compositeFilterDescriptor.Logic, query);
}
private QueryContainerDescriptor<T> ApplyQuery(
IEnumerable<CompositeFilterDescriptor> filterFilters, string filterOperator,
QueryContainerDescriptor<T> query)
{
List<QueryContainerDescriptor<T>> innerQueries =
new List<QueryContainerDescriptor<T>>();
foreach (var filter in filterFilters)
{
if (filter.Filters != null && filter.Filters.Any())
{
innerQueries.Add(ApplyQuery(filter.Filters, filter.Logic, query));
}
else
{
innerQueries.Add((QueryContainerDescriptor<T>)CreateQuery(filter.Field, filter.Operator, filter.IgnoreCase, filter.Logic, filter.Value, query));
}
}
if (filterOperator == "and")
{
return (QueryContainerDescriptor<T>)query.Bool(b => b.Must(innerQueries.Cast<QueryContainer>().ToArray()));
}
else if (filterOperator == "or")
{
return (QueryContainerDescriptor<T>) query.Bool(b => b.Should(innerQueries.Cast<QueryContainer>().ToArray()));
}
else
{
throw new ArgumentOutOfRangeException();
}
}
private QueryContainer CreateQuery(string filterField,
string filterOperator,bool? filterIgnoreCase, string filterLogic, object value,
QueryContainerDescriptor<T> query)
{
var type = MapToType(filterField);
var propertyExpression = MapToProperty(filterField);
return CreateQuery(query,type, propertyExpression, filterOperator, value);
}
private QueryContainer CreateQuery(QueryContainerDescriptor<T> query,
Type fieldType, Expression<Func<T, object>> propertyExpression,
string filterOperator, object value)
{
if (fieldType == typeof(String))
{
return CreateStringFilter(query,propertyExpression, filterOperator, value);
}
else if (fieldType == typeof(DateTime))
{
return CreateDateTimeFilter(query, propertyExpression, filterOperator, value);
}
else
{
throw new ArgumentOutOfRangeException();
}
}
private QueryContainer CreateDateTimeFilter(QueryContainerDescriptor<ElasticDataUpload<SystemLog>> query,
Expression<Func<ElasticDataUpload<SystemLog>, object>> propertyExpression, string filterOperator,
object value)
{
switch (filterOperator)
{
case "eq":
return query.DateRange(d => d.Field(propertyExpression)
.GreaterThanOrEquals(DateMath.Anchored((DateTime)value))
.LessThanOrEquals(DateMath.Anchored((DateTime)value)));
case "neq":
return query.DateRange(d => d.Field(propertyExpression)
.GreaterThanOrEquals(DateMath.Anchored((DateTime)value))
.LessThanOrEquals(DateMath.Anchored((DateTime)value)));
case "isnull":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "isnotnull":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "lt":
return query.DateRange(d => d.Field(propertyExpression).LessThan(DateMath.Anchored((DateTime)value)));
case "lte":
return query.DateRange(d => d.Field(propertyExpression).LessThanOrEquals(DateMath.Anchored((DateTime)value)));
case "gt":
return query.DateRange(d => d.Field(propertyExpression).GreaterThan(DateMath.Anchored((DateTime)value)));
case "gte":
return query.DateRange(d => d.Field(propertyExpression).GreaterThanOrEquals(DateMath.Anchored((DateTime)value)));
case "startswith":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "endswith":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "contains":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "doesnotcontain":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "isempty":
throw new ArgumentException("Unable to apply less than to date and time queries");
case "isnotempty":
throw new ArgumentException("Unable to apply less than to date and time queries");
default:
throw new ArgumentOutOfRangeException();
}
}
private QueryContainer CreateStringFilter(QueryContainerDescriptor<T> query,
Expression<Func<T, object>> propertyExpression,
string filterOperator, object value)
{
switch (filterOperator)
{
case "eq":
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query((string)value));
case "neq":
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query((string)value))));
case "isnull":
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(null));
case "isnotnull":
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(null))));
case "lt":
throw new ArgumentException("Unable to apply less than to string queries");
case "lte":
throw new ArgumentException("Unable to apply less than equal to string queries");
case "gt":
throw new ArgumentException("Unable to apply greater than to string queries");
case "gte":
throw new ArgumentException("Unable to apply greater than equal to string queries");
case "startswith":
return query.Bool(b => b.Should(x => x.Term(propertyExpression, value)));
case "endswith":
return query.Bool(b => b.Should(x => x.Term(propertyExpression, value)));
case "contains":
return query.Match(qs => qs.Field(propertyExpression).Query((string)value).Operator(Operator.And));
case "doesnotcontain":
return query.Bool(b => b.MustNot(x => x.Match(qs => qs.Field(propertyExpression).Query((string)value).Operator(Operator.And))));
case "isempty":
return query.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(""));
case "isnotempty":
return query.Bool(b => b.MustNot(x => x.QueryString(qs => qs.Fields(fs => fs.Field(propertyExpression)).Query(""))));
default:
throw new ArgumentOutOfRangeException();
}
}
private System.Linq.Expressions.Expression<Func<T, object>> MapToProperty(string fieldName)
{
switch (fieldName)
{
//return expressions from property names
default:
throw new ArgumentOutOfRangeException();
}
}
private Type MapToType(string fieldName)
{
switch (fieldName)
{
// Map field name to type
// return typeof(String);
default:
throw new ArgumentOutOfRangeException();
}
}
}

You will need to build in your own express and type mapping for properties, but otherwise this is built for Strings and DateTimes. From this base you should be able to implement different types and queries you would need for the Kendo Grid to work with ElasticSearch.