hurricane/security_camera/camera.c

/*******************************************************************************
* # License
* Copyright 2019 Silicon Laboratories Inc. www.silabs.com
*******************************************************************************
*
* The licensor of this software is Silicon Laboratories Inc. Your use of this
* software is governed by the terms of Silicon Labs Master Software License
* Agreement (MSLA) available at
* www.silabs.com/about-us/legal/master-software-license-agreement. This
* software is distributed to you in Source Code format and is governed by the
* sections of the MSLA applicable to Source Code.
*
******************************************************************************/
#include "common.h"
#define CAMERA_SETTING(name) camera_settings->setting[ARDUCAM_SETTING_ ## name]
#define RESPONSE_TIMEOUT_MS 10000
static struct
{
gos_msgpack_context_t *write_context;
uint32_t next_image_size;
uint32_t start_time;
bool enabled;
bool broadcast_settings_after_image;
} camera_context;
static gos_system_monitor_t camera_monitor;
GOS_CMD_CREATE_COMMAND(camera, trigger, "camera_trigger", "ctrig", 0, 0, false);
/*************************************************************************************************
* Initialize the Arducam
*/
gos_result_t camera_init(void)
{
gos_result_t result;
const arducam_config_t arducam_config =
{
.spi.port = GOS_SPI_1,
.spi.cs = HURRICAME_CAMERA_CS_GPIO,
.i2c_port = PLATFORM_STD_I2C,
.max_read_length = CAMERA_BUFFER_LENGTH,
.driver_config =
{
.resolution = camera_settings->setting[ARDUCAM_SETTING_RESOLUTION],
.format = ARDUCAM_FORMAT_JPEG,
.jpeg_quality = camera_settings->setting[ARDUCAM_SETTING_QUALITY]
},
.callback =
{
.data_writer = arducam_data_writer_callback,
.image_ready = arducam_image_ready_callback,
.error_handler = arducam_error_callback
}
};
if(GOS_FAILED(result, arducam_init(&arducam_config, ARDUCAM_TYPE_OV2640)))
{
}
else
{
arducam_set_setting(ARDUCAM_SETTING_BRIGHTNESS, CAMERA_SETTING(BRIGHTNESS));
arducam_set_setting(ARDUCAM_SETTING_CONTRAST, CAMERA_SETTING(CONTRAST));
arducam_set_setting(ARDUCAM_SETTING_SATURATION, CAMERA_SETTING(SATURATION));
arducam_set_setting(ARDUCAM_SETTING_MIRROR, CAMERA_SETTING(MIRROR));
arducam_set_setting(ARDUCAM_SETTING_FLIP, CAMERA_SETTING(FLIP));
arducam_set_setting(ARDUCAM_SETTING_SPECIALEFFECT, CAMERA_SETTING(SPECIALEFFECT));
}
return result;
}
/*************************************************************************************************
* Enable/disable the camera
*/
void camera_set_video_enabled(bool enabled)
{
sensor_set_enabled(enabled);
if(enabled && !camera_context.enabled)
{
GOS_LOG("Starting image capture");
hurricane_camera_set_led_enabled(true);
camera_context.enabled = true;
gos_register_system_monitor(&camera_monitor, (camera_settings->update_interval_ms * 3) / 2, NULL);
gos_event_issue(start_image_capture_handler, NULL, GOS_EVENT_FLAG_NONE);
}
else if(!enabled && camera_context.enabled)
{
GOS_LOG("Stopping image capture");
camera_context.enabled = false;
hurricane_camera_set_led_enabled(false);
gos_event_issue(cleanup_write_context, NULL, GOS_EVENT_FLAG_IN_NETWORK_WORKER);
arducam_abort_capture();
gos_event_unregister(start_image_capture_handler, NULL);
}
}
/*************************************************************************************************
* Return if the camera is currently enabled
*/
bool camera_is_enabled(void)
{
return camera_context.enabled;
}
/*************************************************************************************************
* Invoke the camera to immediately capture an image
*/
void camera_trigger(void *unused)
{
if(camera_context.enabled)
{
sensor_trigger();
start_image_capture_handler(NULL);
}
}
/*************************************************************************************************
* Return the number of ms until the next image capture
*/
uint32_t camera_get_ms_until_next_capture(void)
{
const uint32_t elapsed = gos_rtos_get_time() - camera_context.start_time;
const uint32_t next_timeout = (camera_settings->update_interval_ms > elapsed) ? camera_settings->update_interval_ms - elapsed : 0;
return next_timeout;
}
/*************************************************************************************************
* This is called by the adrucam driver when image data is available
* It executes in the network worker context
*
* This used the DMS message API to send a message containing the image data and metadata
* to the DMS product's webhook (which then forwards to Firebase).
*
* This callback is called for each of image data read from the camera.
* The number of times it's called is dependent on the image size.
*/
static gos_result_t arducam_data_writer_callback(const void *image_chunk, uint32_t length, bool last_chunk)
{
gos_result_t result;
// If the camera is no longer enabled
if(!camera_context.enabled)
{
// Then abort
result = GOS_ABORTED;
}
// If we have a new image available
// Then initialize a new msg context and write the header
else if(camera_context.next_image_size != 0)
{
const uint32_t image_size = camera_context.next_image_size;
// Clear it to indicate we've allocated a write context for it
camera_context.next_image_size = 0;
camera_context.start_time = gos_rtos_get_time();
uint8_t msg_header_buffer[384];
char uuid_str[GOS_DEVICE_UUID_LEN*2+1];
gos_utc_seconds_t timestamp;
{
.length = 0,
.response.handler = message_response_handler,
.response.timeout_ms = RESPONSE_TIMEOUT_MS
};
MSGPACK_INIT_WITH_BUFFER(msgpack_header, msg_header_buffer, sizeof(msg_header_buffer));
// NOTE: The DMS msg APIs MUST use 16dicts
msgpack_header.flags = MSGPACK_PACK_16BIT_DICTS;
/*
* Write the image message header:
* {
* request: "webbook",
* code: "IMAGE",
* data: {
* uuid: "<device UUID string>",
* code: "<device registration code>",
* timestamp: <UTC timestamp in seconds>,
* meta: {
* latitude: <gps-latitude deg>,
* longitude: <gps-longitude deg>,
* temperature: <temperature celcius>,
* humidity: <humidity>,
* uvi: <UV index>,
* pressure: <pressure>,
* als: <ambient light>
* },
* img: "<binary image data>" // MUST come last
* }
* }
*/
gos_msgpack_write_dict_marker(&msgpack_header, 3);
gos_msgpack_write_dict_str(&msgpack_header, "request", "webhook");
gos_msgpack_write_dict_str(&msgpack_header, "code", IMAGE_WEBHOOK_CODE);
gos_msgpack_write_dict_dict(&msgpack_header, "data", 5);
gos_msgpack_write_dict_str(&msgpack_header, "code", device_registration_code);
gos_msgpack_write_dict_str(&msgpack_header, "uuid", gos_system_get_uuid_str(uuid_str));
gos_msgpack_write_dict_uint(&msgpack_header, "timestamp", timestamp);
gos_msgpack_write_str(&msgpack_header, "meta");
sensor_read_all(&msgpack_header);
gos_msgpack_write_str(&msgpack_header, "img");
gos_msgpack_write_bin_marker(&msgpack_header, image_size);
const uint32_t msgpack_header_length = MSGPACK_BUFFER_USED(&msgpack_header);
// Set the length to make this message a fixed length-message
// (as opposed to variable-length message)
config.length = msgpack_header_length + image_size;
// Just for good measure cleanup the old context
// Before we update the write_context pointer with a new one
cleanup_write_context(NULL);
// Initialize the message context
if(GOS_FAILED(result, gos_dms_message_write_init(&camera_context.write_context, &config)))
{
GOS_LOG("Failed to init image write context, err: %d", result);
}
// Write the message header
else if(GOS_FAILED(result, gos_dms_message_write_raw(camera_context.write_context, msg_header_buffer, msgpack_header_length)))
{
GOS_LOG("Failed to write image header, err: %d", result);
goto exit;
}
}
// Write the image data chunk to the message context
// This will be called for each chunk of the image
if(GOS_FAILED(result, gos_dms_message_write_raw(camera_context.write_context, image_chunk, length)))
{
GOS_LOG("Failed to write image chunk, err:%d", result);
}
// If this was the last chunk
else if(last_chunk)
{
// Then flush any remaining data
if(GOS_FAILED(result, gos_dms_message_write_flush(camera_context.write_context)))
{
GOS_LOG("Failed to flush last chunk, err: %d", result);
}
// Clear the pointer to the write context since it is no longer valid
camera_context.write_context = NULL;
}
exit:
// If something failed then cleanup the write context
if(result != GOS_SUCCESS)
{
arducam_abort_capture();
cleanup_write_context(NULL);
}
// If image capture is still enabled AND
// we just sent the last chunk OR
// something failed writing the previous image
if(camera_context.enabled && (last_chunk || result != GOS_SUCCESS))
{
const uint32_t next_timeout = (result == GOS_SUCCESS) ? camera_get_ms_until_next_capture() : 15000;
gos_update_system_monitor(&camera_monitor, (camera_settings->update_interval_ms * 3) / 2);
// Then start another image capture in the application thread
gos_event_register_timed(start_image_capture_handler, NULL, next_timeout, GOS_EVENT_FLAG_NONE);
}
return result;
}
/*************************************************************************************************
* This is called when a response to the image message sent above is received from the DMS
* This executes in the app context.
*
* The response contains the response from the Firebase /addImage cloud function.
*/
static void message_response_handler(void *msg)
{
const gos_msgpack_object_dict_t *data_obj = MSGPACK_DICT_DICT(msg, "data");
if(data_obj != NULL)
{
bool settings_updated = false;
const gos_msgpack_object_t *brightness_obj = MSGPACK_DICT_INT(data_obj, "brightness");
const gos_msgpack_object_t *contrast_obj = MSGPACK_DICT_INT(data_obj, "contrast");
const gos_msgpack_object_t *saturation_obj = MSGPACK_DICT_INT(data_obj, "saturation");
const gos_msgpack_object_t *mirror_obj = MSGPACK_DICT_INT(data_obj, "mirror");
const gos_msgpack_object_t *flip_obj = MSGPACK_DICT_INT(data_obj, "flip");
const gos_msgpack_object_t *filter_obj = MSGPACK_DICT_INT(data_obj, "filter");
const gos_msgpack_object_t *quality_obj = MSGPACK_DICT_INT(data_obj, "quality");
const gos_msgpack_object_t *resolution_obj = MSGPACK_DICT_INT(data_obj, "resolution");
const gos_msgpack_object_t *interval_obj = MSGPACK_DICT_INT(data_obj, "interval");
settings_updated |= update_setting(ARDUCAM_SETTING_BRIGHTNESS, brightness_obj);
settings_updated |= update_setting(ARDUCAM_SETTING_CONTRAST, contrast_obj);
settings_updated |= update_setting(ARDUCAM_SETTING_SATURATION, saturation_obj);
settings_updated |= update_setting(ARDUCAM_SETTING_MIRROR, mirror_obj);
settings_updated |= update_setting(ARDUCAM_SETTING_FLIP, flip_obj);
settings_updated |= update_setting(ARDUCAM_SETTING_SPECIALEFFECT, filter_obj);
settings_updated |= update_setting(ARDUCAM_SETTING_QUALITY, quality_obj);
settings_updated |= update_setting(ARDUCAM_SETTING_RESOLUTION, resolution_obj);
if(settings_updated)
{
GOS_LOG("Settings updated");
}
if(interval_obj != NULL)
{
const uint32_t update_interval = MAX(MSGPACK_INT(interval_obj), MIN_UPDATE_INTERVAL_MS);
if(update_interval != camera_settings->update_interval_ms)
{
GOS_LOG("New update interval: %d.%03ds", update_interval / 1000, update_interval % 1000);
camera_settings->update_interval_ms = update_interval;
}
}
}
// Clean up message
}
/*************************************************************************************************
* This is called if an error is encountered in the camera library
*/
static void arducam_error_callback(gos_result_t result)
{
GOS_LOG("Arducam library error: %d", result);
}
/*************************************************************************************************
* This is called when a new image is ready to be read from the Arducam
*/
static bool arducam_image_ready_callback(uint32_t image_size)
{
// GOS_LOG("Next image size: %d", image_size);
camera_context.next_image_size = image_size;
// Return true to indicate that the image to start being read
return true;
}
/*************************************************************************************************
* Trigger an image capture
*
* This executes in the appliation thread
*/
static void start_image_capture_handler(void *unused)
{
arducam_start_capture();
}
/*************************************************************************************************
* Clean up the write context
*
* This executes in the network thread
*/
static void cleanup_write_context(void *unused)
{
if(camera_context.write_context != NULL)
{
gos_dms_message_context_destroy(camera_context.write_context);
camera_context.write_context = NULL;
}
}
/*************************************************************************************************/
static bool update_setting(arducam_setting_type_t setting, const gos_msgpack_object_t *obj)
{
bool retval = false;
if(obj != NULL)
{
const int32_t value = MSGPACK_INT(obj);
if(camera_settings->setting[setting] != value)
{
if(arducam_set_setting(setting, value) == GOS_SUCCESS)
{
camera_settings->setting[setting] = value;
retval = true;
}
}
}
return retval;
}
/*************************************************************************************************/
static gos_cmd_result_t camera_trigger_command(int argc, char **argv)
{
if(camera_context.enabled)
{
gos_event_issue(camera_trigger, NULL, GOS_EVENT_FLAG_NONE);
}
else
{
}
}