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:
- RSA Security Research – by Mark Stanislav
- Will it hack
- Azure IoT Edge product page
- VLC download page
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
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.