OV528 Camera Module Library
CameraOV528.cpp@3:274014e87a49, 2019-02-02 (annotated)
- Committer:
- aoba
- Date:
- Sat Feb 02 05:22:29 2019 +0000
- Revision:
- 3:274014e87a49
- Parent:
- 2:7a563410b23d
Changed the constant name for collision on recent Mbed
Who changed what in which revision?
User | Revision | Line number | New contents of line |
---|---|---|---|
jplunkett | 0:3f9d7ef7266c | 1 | /* Copyright (c) 2016 ARM Limited |
jplunkett | 0:3f9d7ef7266c | 2 | * |
jplunkett | 0:3f9d7ef7266c | 3 | * Licensed under the Apache License, Version 2.0 (the "License"); |
jplunkett | 0:3f9d7ef7266c | 4 | * you may not use this file except in compliance with the License. |
jplunkett | 0:3f9d7ef7266c | 5 | * You may obtain a copy of the License at |
jplunkett | 0:3f9d7ef7266c | 6 | * |
jplunkett | 0:3f9d7ef7266c | 7 | * http://www.apache.org/licenses/LICENSE-2.0 |
jplunkett | 0:3f9d7ef7266c | 8 | * |
jplunkett | 0:3f9d7ef7266c | 9 | * Unless required by applicable law or agreed to in writing, software |
jplunkett | 0:3f9d7ef7266c | 10 | * distributed under the License is distributed on an "AS IS" BASIS, |
jplunkett | 0:3f9d7ef7266c | 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
jplunkett | 0:3f9d7ef7266c | 12 | * See the License for the specific language governing permissions and |
jplunkett | 0:3f9d7ef7266c | 13 | * limitations under the License. |
jplunkett | 0:3f9d7ef7266c | 14 | */ |
jplunkett | 0:3f9d7ef7266c | 15 | |
jplunkett | 0:3f9d7ef7266c | 16 | /* |
jplunkett | 0:3f9d7ef7266c | 17 | * Originally Based on the (Grove_Serial_Camera_Kit) - https://github.com/Seeed-Studio/Grove_Serial_Camera_Kit |
jplunkett | 0:3f9d7ef7266c | 18 | */ |
jplunkett | 0:3f9d7ef7266c | 19 | |
jplunkett | 0:3f9d7ef7266c | 20 | #include<stdint.h> |
jplunkett | 0:3f9d7ef7266c | 21 | #include<stdlib.h> |
jplunkett | 0:3f9d7ef7266c | 22 | #include<string.h> |
jplunkett | 0:3f9d7ef7266c | 23 | |
jplunkett | 0:3f9d7ef7266c | 24 | #include "CameraOV528.h" |
jplunkett | 0:3f9d7ef7266c | 25 | |
jplunkett | 0:3f9d7ef7266c | 26 | #define camera_printf(...) |
jplunkett | 0:3f9d7ef7266c | 27 | |
jplunkett | 0:3f9d7ef7266c | 28 | #define PIC_CLOCK_HZ 14745600 |
jplunkett | 0:3f9d7ef7266c | 29 | #define BOOT_BAUD 9600 |
jplunkett | 0:3f9d7ef7266c | 30 | |
jplunkett | 0:3f9d7ef7266c | 31 | #define COMMAND_LENGTH 6 |
jplunkett | 0:3f9d7ef7266c | 32 | #define PIC_PAYLOAD_DATA_BEGIN 4 |
jplunkett | 0:3f9d7ef7266c | 33 | #define PIC_PAYLOAD_OVERHEAD 6 |
jplunkett | 0:3f9d7ef7266c | 34 | #define READ_WAKE_POS_INVALID 0xFFFFFFFF |
jplunkett | 0:3f9d7ef7266c | 35 | |
jplunkett | 0:3f9d7ef7266c | 36 | #define DEFAULT_BAUD 115200 |
jplunkett | 0:3f9d7ef7266c | 37 | |
jplunkett | 0:3f9d7ef7266c | 38 | enum CameraCommand { |
jplunkett | 0:3f9d7ef7266c | 39 | INITIAL = 0x01, |
jplunkett | 0:3f9d7ef7266c | 40 | GET_PICTURE = 0x04, |
jplunkett | 0:3f9d7ef7266c | 41 | SNAPSHOT = 0x05, |
jplunkett | 0:3f9d7ef7266c | 42 | SET_PACKAGE_SIZE = 0x06, |
jplunkett | 0:3f9d7ef7266c | 43 | SET_BAUD_RATE = 0x07, |
aoba | 3:274014e87a49 | 44 | RESET_CAMERA = 0x08, |
jplunkett | 0:3f9d7ef7266c | 45 | POWER_DOWN = 0x09, |
jplunkett | 0:3f9d7ef7266c | 46 | DATA = 0x0A, |
jplunkett | 0:3f9d7ef7266c | 47 | SYNC = 0x0D, |
jplunkett | 0:3f9d7ef7266c | 48 | ACK = 0x0E, |
jplunkett | 0:3f9d7ef7266c | 49 | NAK = 0x0F |
jplunkett | 0:3f9d7ef7266c | 50 | }; |
jplunkett | 0:3f9d7ef7266c | 51 | |
jplunkett | 0:3f9d7ef7266c | 52 | enum GetSetting { |
jplunkett | 0:3f9d7ef7266c | 53 | GET_SNAPSHOT = 1, |
jplunkett | 0:3f9d7ef7266c | 54 | GET_PREVIEW_PICTURE = 2, |
jplunkett | 0:3f9d7ef7266c | 55 | GET_JPEG_PREVIEW_PICTURE = 3, |
jplunkett | 0:3f9d7ef7266c | 56 | }; |
jplunkett | 0:3f9d7ef7266c | 57 | |
jplunkett | 0:3f9d7ef7266c | 58 | typedef struct { |
jplunkett | 0:3f9d7ef7266c | 59 | uint8_t header; |
jplunkett | 0:3f9d7ef7266c | 60 | uint8_t command; |
jplunkett | 0:3f9d7ef7266c | 61 | uint8_t param[4]; |
jplunkett | 0:3f9d7ef7266c | 62 | } camera_command_t; |
jplunkett | 0:3f9d7ef7266c | 63 | |
jplunkett | 0:3f9d7ef7266c | 64 | CameraOV528::Resolution supported_resolutions[] = { |
jplunkett | 0:3f9d7ef7266c | 65 | // CameraOV528::RES_80x60, //Does not work |
jplunkett | 0:3f9d7ef7266c | 66 | CameraOV528::RES_160x120, |
jplunkett | 0:3f9d7ef7266c | 67 | CameraOV528::RES_320x240, |
jplunkett | 0:3f9d7ef7266c | 68 | CameraOV528::RES_640x480, |
jplunkett | 0:3f9d7ef7266c | 69 | }; |
jplunkett | 0:3f9d7ef7266c | 70 | |
jplunkett | 0:3f9d7ef7266c | 71 | CameraOV528::Format supported_formats[] = { |
jplunkett | 0:3f9d7ef7266c | 72 | // CameraOV528::FMT_2_BIT_GRAY_SCALE, //Does not work |
jplunkett | 0:3f9d7ef7266c | 73 | // CameraOV528::FMT_4_BIT_GRAY_SCALE, |
jplunkett | 0:3f9d7ef7266c | 74 | // CameraOV528::FMT_8_BIT_GRAY_SCALE, |
jplunkett | 0:3f9d7ef7266c | 75 | // CameraOV528::FMT_2_BIT_COLOR, |
jplunkett | 0:3f9d7ef7266c | 76 | // CameraOV528::FMT_16_BIT, |
jplunkett | 0:3f9d7ef7266c | 77 | CameraOV528::FMT_JPEG, |
jplunkett | 0:3f9d7ef7266c | 78 | }; |
jplunkett | 0:3f9d7ef7266c | 79 | |
jplunkett | 0:3f9d7ef7266c | 80 | static uint32_t divide_round_up(uint32_t dividen, uint32_t divisor); |
jplunkett | 0:3f9d7ef7266c | 81 | static uint32_t min(uint32_t value1, uint32_t value2); |
jplunkett | 0:3f9d7ef7266c | 82 | static uint32_t queue_used(uint32_t head, uint32_t tail, uint32_t size); |
jplunkett | 0:3f9d7ef7266c | 83 | |
jplunkett | 0:3f9d7ef7266c | 84 | CameraOV528::CameraOV528(PinName rx, PinName tx) : _serial(rx, tx), _read_sem(0) |
jplunkett | 0:3f9d7ef7266c | 85 | { |
jplunkett | 0:3f9d7ef7266c | 86 | _init_done = false; |
jplunkett | 0:3f9d7ef7266c | 87 | _resolution = RES_640x480; |
jplunkett | 0:3f9d7ef7266c | 88 | _format = FMT_JPEG; |
jplunkett | 0:3f9d7ef7266c | 89 | _baud = DEFAULT_BAUD; |
jplunkett | 0:3f9d7ef7266c | 90 | |
jplunkett | 0:3f9d7ef7266c | 91 | _dev_resolution = _resolution; |
jplunkett | 0:3f9d7ef7266c | 92 | _dev_format = _format; |
jplunkett | 0:3f9d7ef7266c | 93 | _dev_baud = _baud; |
jplunkett | 0:3f9d7ef7266c | 94 | |
jplunkett | 0:3f9d7ef7266c | 95 | picture_length = 0; |
jplunkett | 0:3f9d7ef7266c | 96 | picture_data_id = 0; |
jplunkett | 0:3f9d7ef7266c | 97 | picture_data_id_count = 0; |
jplunkett | 0:3f9d7ef7266c | 98 | memset(picture_buffer, 0, sizeof(picture_buffer)); |
jplunkett | 0:3f9d7ef7266c | 99 | picture_buffer_size_limit = sizeof(picture_buffer); |
jplunkett | 0:3f9d7ef7266c | 100 | picture_buffer_pos = 0; |
jplunkett | 0:3f9d7ef7266c | 101 | picture_buffer_size = 0; |
jplunkett | 0:3f9d7ef7266c | 102 | |
jplunkett | 0:3f9d7ef7266c | 103 | memset(_read_buf, 0, sizeof(_read_buf)); |
jplunkett | 0:3f9d7ef7266c | 104 | _read_buf_head = 0; |
jplunkett | 0:3f9d7ef7266c | 105 | _read_buf_tail = 0; |
jplunkett | 0:3f9d7ef7266c | 106 | _read_wake_pos = READ_WAKE_POS_INVALID; |
jplunkett | 1:06afc809909b | 107 | this->log_func = debug; |
jplunkett | 0:3f9d7ef7266c | 108 | } |
jplunkett | 0:3f9d7ef7266c | 109 | |
jplunkett | 0:3f9d7ef7266c | 110 | CameraOV528::~CameraOV528() |
jplunkett | 0:3f9d7ef7266c | 111 | { |
jplunkett | 0:3f9d7ef7266c | 112 | powerdown(); |
jplunkett | 0:3f9d7ef7266c | 113 | } |
jplunkett | 0:3f9d7ef7266c | 114 | |
jplunkett | 1:06afc809909b | 115 | void CameraOV528::attach_debug_function(void (*func)(const char* fmt, ...)) |
jplunkett | 1:06afc809909b | 116 | { |
jplunkett | 1:06afc809909b | 117 | this->log_func = func; |
jplunkett | 1:06afc809909b | 118 | } |
jplunkett | 1:06afc809909b | 119 | |
jplunkett | 1:06afc809909b | 120 | int CameraOV528::camera_error(const char* message) |
jplunkett | 1:06afc809909b | 121 | { |
jplunkett | 1:06afc809909b | 122 | log_func("%s\r\n",message); |
jplunkett | 1:06afc809909b | 123 | return -1; |
jplunkett | 1:06afc809909b | 124 | } |
jplunkett | 1:06afc809909b | 125 | |
jplunkett | 1:06afc809909b | 126 | int CameraOV528::powerup() |
jplunkett | 0:3f9d7ef7266c | 127 | { |
jplunkett | 0:3f9d7ef7266c | 128 | if (_init_done) { |
jplunkett | 1:06afc809909b | 129 | return 0; |
jplunkett | 0:3f9d7ef7266c | 130 | } |
jplunkett | 0:3f9d7ef7266c | 131 | |
jplunkett | 0:3f9d7ef7266c | 132 | _serial.attach(this, &CameraOV528::_rx_irq, SerialBase::RxIrq); |
jplunkett | 0:3f9d7ef7266c | 133 | |
jplunkett | 0:3f9d7ef7266c | 134 | // Attempt to connect at the desired baud |
jplunkett | 0:3f9d7ef7266c | 135 | _serial.baud(_baud); |
jplunkett | 0:3f9d7ef7266c | 136 | bool success = _init_sequence(); |
jplunkett | 0:3f9d7ef7266c | 137 | |
jplunkett | 0:3f9d7ef7266c | 138 | // If unsuccessful try with boot baud |
jplunkett | 0:3f9d7ef7266c | 139 | if (!success) { |
jplunkett | 0:3f9d7ef7266c | 140 | _serial.baud(BOOT_BAUD); |
jplunkett | 0:3f9d7ef7266c | 141 | success = _init_sequence(); |
jplunkett | 0:3f9d7ef7266c | 142 | } |
jplunkett | 0:3f9d7ef7266c | 143 | |
jplunkett | 0:3f9d7ef7266c | 144 | if (!success) { |
jplunkett | 1:06afc809909b | 145 | return camera_error("Unable to communicate with camera"); |
jplunkett | 0:3f9d7ef7266c | 146 | } |
jplunkett | 0:3f9d7ef7266c | 147 | |
jplunkett | 0:3f9d7ef7266c | 148 | // Acknowledge the SYNC read in _init_sequence with an ACK |
jplunkett | 0:3f9d7ef7266c | 149 | _send_ack(SYNC, 0, 0, 0); |
jplunkett | 0:3f9d7ef7266c | 150 | camera_printf("\nCamera initialization done.\r\n"); |
jplunkett | 0:3f9d7ef7266c | 151 | |
jplunkett | 0:3f9d7ef7266c | 152 | _set_baud(_baud); |
jplunkett | 0:3f9d7ef7266c | 153 | |
jplunkett | 0:3f9d7ef7266c | 154 | _set_fmt_and_res(_format, _resolution); |
jplunkett | 0:3f9d7ef7266c | 155 | _set_package_size(picture_buffer_size_limit); |
jplunkett | 0:3f9d7ef7266c | 156 | |
jplunkett | 0:3f9d7ef7266c | 157 | _init_done = false; |
jplunkett | 1:06afc809909b | 158 | return 0; |
jplunkett | 0:3f9d7ef7266c | 159 | } |
jplunkett | 0:3f9d7ef7266c | 160 | |
jplunkett | 1:06afc809909b | 161 | int CameraOV528::powerup(uint32_t baud) |
jplunkett | 0:3f9d7ef7266c | 162 | { |
jplunkett | 0:3f9d7ef7266c | 163 | _baud = baud; |
jplunkett | 1:06afc809909b | 164 | return powerup(); |
jplunkett | 0:3f9d7ef7266c | 165 | } |
jplunkett | 0:3f9d7ef7266c | 166 | |
jplunkett | 1:06afc809909b | 167 | int CameraOV528::powerdown() |
jplunkett | 0:3f9d7ef7266c | 168 | { |
jplunkett | 0:3f9d7ef7266c | 169 | if (!_init_done) { |
jplunkett | 1:06afc809909b | 170 | return 0; |
jplunkett | 0:3f9d7ef7266c | 171 | } |
jplunkett | 0:3f9d7ef7266c | 172 | |
jplunkett | 0:3f9d7ef7266c | 173 | if (!_send_cmd(POWER_DOWN, 0)) { |
jplunkett | 1:06afc809909b | 174 | return camera_error("Powerdown failed"); |
jplunkett | 0:3f9d7ef7266c | 175 | } |
jplunkett | 0:3f9d7ef7266c | 176 | |
jplunkett | 0:3f9d7ef7266c | 177 | // Reset picture transfer variables |
jplunkett | 0:3f9d7ef7266c | 178 | picture_length = 0; |
jplunkett | 0:3f9d7ef7266c | 179 | picture_data_id = 0; |
jplunkett | 0:3f9d7ef7266c | 180 | picture_data_id_count = 0; |
jplunkett | 0:3f9d7ef7266c | 181 | memset(picture_buffer, 0, sizeof(picture_buffer)); |
jplunkett | 0:3f9d7ef7266c | 182 | picture_buffer_size_limit = sizeof(picture_buffer); |
jplunkett | 0:3f9d7ef7266c | 183 | picture_buffer_pos = 0; |
jplunkett | 0:3f9d7ef7266c | 184 | picture_buffer_size = 0; |
jplunkett | 0:3f9d7ef7266c | 185 | |
jplunkett | 0:3f9d7ef7266c | 186 | _serial.attach(NULL, SerialBase::RxIrq); |
jplunkett | 0:3f9d7ef7266c | 187 | _flush_rx(); |
jplunkett | 0:3f9d7ef7266c | 188 | |
jplunkett | 0:3f9d7ef7266c | 189 | _init_done = false; |
jplunkett | 1:06afc809909b | 190 | return 0; |
jplunkett | 0:3f9d7ef7266c | 191 | } |
jplunkett | 0:3f9d7ef7266c | 192 | |
jplunkett | 1:06afc809909b | 193 | int CameraOV528::take_picture(void) |
jplunkett | 0:3f9d7ef7266c | 194 | { |
jplunkett | 0:3f9d7ef7266c | 195 | // Ensure driver is powered up |
jplunkett | 0:3f9d7ef7266c | 196 | powerup(); |
jplunkett | 0:3f9d7ef7266c | 197 | |
jplunkett | 0:3f9d7ef7266c | 198 | // Update settings if any have changed |
jplunkett | 0:3f9d7ef7266c | 199 | if ((_dev_format != _format) || (_dev_resolution != _resolution)) { |
jplunkett | 0:3f9d7ef7266c | 200 | _set_fmt_and_res(_format, _resolution); |
jplunkett | 0:3f9d7ef7266c | 201 | _dev_format = _format; |
jplunkett | 0:3f9d7ef7266c | 202 | _dev_resolution = _resolution; |
jplunkett | 0:3f9d7ef7266c | 203 | } |
jplunkett | 0:3f9d7ef7266c | 204 | |
jplunkett | 0:3f9d7ef7266c | 205 | // Take snapshot |
jplunkett | 0:3f9d7ef7266c | 206 | camera_printf("Taking snapshot\r\n"); |
jplunkett | 0:3f9d7ef7266c | 207 | if (!_send_cmd(SNAPSHOT, 0)) { |
jplunkett | 1:06afc809909b | 208 | return camera_error("Take snapshot failed"); |
jplunkett | 0:3f9d7ef7266c | 209 | } |
jplunkett | 0:3f9d7ef7266c | 210 | |
jplunkett | 0:3f9d7ef7266c | 211 | // Start picture transfer |
jplunkett | 0:3f9d7ef7266c | 212 | camera_printf("Starting transfer\r\n"); |
jplunkett | 0:3f9d7ef7266c | 213 | const GetSetting request = GET_JPEG_PREVIEW_PICTURE; |
jplunkett | 0:3f9d7ef7266c | 214 | if (!_send_cmd(GET_PICTURE, request)) { |
jplunkett | 1:06afc809909b | 215 | return camera_error("Get picture command failed"); |
jplunkett | 0:3f9d7ef7266c | 216 | } |
jplunkett | 0:3f9d7ef7266c | 217 | camera_command_t resp = {0}; |
jplunkett | 0:3f9d7ef7266c | 218 | uint32_t size_read = _read((uint8_t*)&resp, COMMAND_LENGTH, 1000); |
jplunkett | 0:3f9d7ef7266c | 219 | if (size_read != COMMAND_LENGTH) { |
jplunkett | 1:06afc809909b | 220 | return camera_error("Get picture response invalid"); |
jplunkett | 0:3f9d7ef7266c | 221 | } |
jplunkett | 1:06afc809909b | 222 | if (resp.header != 0xAA) camera_error("Get picture response invalid sync"); |
jplunkett | 1:06afc809909b | 223 | if (resp.command != DATA) camera_error("Get picture response invalid data"); |
jplunkett | 1:06afc809909b | 224 | if (resp.param[0] != request) camera_error("Get picture response invalid content"); |
jplunkett | 0:3f9d7ef7266c | 225 | picture_length = (resp.param[1] << 0) | |
jplunkett | 0:3f9d7ef7266c | 226 | (resp.param[2] << 8) | |
jplunkett | 0:3f9d7ef7266c | 227 | (resp.param[3] << 16); |
jplunkett | 0:3f9d7ef7266c | 228 | picture_data_id = 0; |
jplunkett | 0:3f9d7ef7266c | 229 | uint32_t payload_length = picture_buffer_size_limit - PIC_PAYLOAD_OVERHEAD; |
jplunkett | 0:3f9d7ef7266c | 230 | picture_data_id_count = divide_round_up(picture_length, payload_length); |
jplunkett | 1:06afc809909b | 231 | return 0; |
jplunkett | 0:3f9d7ef7266c | 232 | } |
jplunkett | 0:3f9d7ef7266c | 233 | |
jplunkett | 0:3f9d7ef7266c | 234 | uint32_t CameraOV528::get_picture_size() |
jplunkett | 0:3f9d7ef7266c | 235 | { |
jplunkett | 0:3f9d7ef7266c | 236 | return picture_length; |
jplunkett | 0:3f9d7ef7266c | 237 | } |
jplunkett | 0:3f9d7ef7266c | 238 | |
jplunkett | 0:3f9d7ef7266c | 239 | uint32_t CameraOV528::read_picture_data(uint8_t * data, uint32_t size) |
jplunkett | 0:3f9d7ef7266c | 240 | { |
jplunkett | 0:3f9d7ef7266c | 241 | uint32_t size_copied = 0; |
jplunkett | 0:3f9d7ef7266c | 242 | while (size_copied < size) { |
jplunkett | 0:3f9d7ef7266c | 243 | |
jplunkett | 0:3f9d7ef7266c | 244 | // Copy any data in the picture buffer |
jplunkett | 0:3f9d7ef7266c | 245 | uint32_t size_left = size - size_copied; |
jplunkett | 0:3f9d7ef7266c | 246 | uint32_t copy_size = min(picture_buffer_size, size_left); |
jplunkett | 0:3f9d7ef7266c | 247 | memcpy(data + size_copied, picture_buffer + picture_buffer_pos, copy_size); |
jplunkett | 0:3f9d7ef7266c | 248 | picture_buffer_pos += copy_size; |
jplunkett | 0:3f9d7ef7266c | 249 | picture_buffer_size -= copy_size; |
jplunkett | 0:3f9d7ef7266c | 250 | size_copied += copy_size; |
jplunkett | 0:3f9d7ef7266c | 251 | |
jplunkett | 0:3f9d7ef7266c | 252 | // If picture buffer is empty read more data |
jplunkett | 0:3f9d7ef7266c | 253 | if (0 == picture_buffer_size) { |
jplunkett | 0:3f9d7ef7266c | 254 | _read_picture_block(); |
jplunkett | 0:3f9d7ef7266c | 255 | } |
jplunkett | 0:3f9d7ef7266c | 256 | |
jplunkett | 0:3f9d7ef7266c | 257 | // If there is still no more data to read then break from loop |
jplunkett | 0:3f9d7ef7266c | 258 | if (0 == picture_buffer_size) { |
jplunkett | 0:3f9d7ef7266c | 259 | break; |
jplunkett | 0:3f9d7ef7266c | 260 | } |
jplunkett | 0:3f9d7ef7266c | 261 | } |
jplunkett | 0:3f9d7ef7266c | 262 | |
jplunkett | 0:3f9d7ef7266c | 263 | return size_copied; |
jplunkett | 0:3f9d7ef7266c | 264 | } |
jplunkett | 0:3f9d7ef7266c | 265 | |
jplunkett | 0:3f9d7ef7266c | 266 | void CameraOV528::set_resolution(CameraOV528::Resolution resolution) |
jplunkett | 0:3f9d7ef7266c | 267 | { |
jplunkett | 0:3f9d7ef7266c | 268 | const uint32_t count = sizeof(supported_resolutions) / sizeof(supported_resolutions[0]); |
jplunkett | 0:3f9d7ef7266c | 269 | bool valid_resolution = false; |
jplunkett | 0:3f9d7ef7266c | 270 | for (uint32_t i = 0; i < count; i++) { |
jplunkett | 0:3f9d7ef7266c | 271 | if (resolution == supported_resolutions[i]) { |
jplunkett | 0:3f9d7ef7266c | 272 | valid_resolution = true; |
jplunkett | 0:3f9d7ef7266c | 273 | break; |
jplunkett | 0:3f9d7ef7266c | 274 | } |
jplunkett | 0:3f9d7ef7266c | 275 | } |
jplunkett | 0:3f9d7ef7266c | 276 | |
jplunkett | 0:3f9d7ef7266c | 277 | if (!valid_resolution) { |
jplunkett | 1:06afc809909b | 278 | camera_error("Invalid resolution"); |
jplunkett | 0:3f9d7ef7266c | 279 | } |
jplunkett | 0:3f9d7ef7266c | 280 | |
jplunkett | 0:3f9d7ef7266c | 281 | _resolution = resolution; |
jplunkett | 0:3f9d7ef7266c | 282 | |
jplunkett | 0:3f9d7ef7266c | 283 | } |
jplunkett | 0:3f9d7ef7266c | 284 | |
jplunkett | 0:3f9d7ef7266c | 285 | void CameraOV528::set_format(CameraOV528::Format format) |
jplunkett | 0:3f9d7ef7266c | 286 | { |
jplunkett | 0:3f9d7ef7266c | 287 | const uint32_t count = sizeof(supported_formats) / sizeof(supported_formats[0]); |
jplunkett | 0:3f9d7ef7266c | 288 | bool valid_format = false; |
jplunkett | 0:3f9d7ef7266c | 289 | for (uint32_t i = 0; i < count; i++) { |
jplunkett | 0:3f9d7ef7266c | 290 | if (format == supported_formats[i]) { |
jplunkett | 0:3f9d7ef7266c | 291 | valid_format = true; |
jplunkett | 0:3f9d7ef7266c | 292 | break; |
jplunkett | 0:3f9d7ef7266c | 293 | } |
jplunkett | 0:3f9d7ef7266c | 294 | } |
jplunkett | 0:3f9d7ef7266c | 295 | |
jplunkett | 0:3f9d7ef7266c | 296 | if (!valid_format) { |
jplunkett | 1:06afc809909b | 297 | camera_error("Invalid format"); |
jplunkett | 0:3f9d7ef7266c | 298 | } |
jplunkett | 0:3f9d7ef7266c | 299 | |
jplunkett | 0:3f9d7ef7266c | 300 | _format = format; |
jplunkett | 0:3f9d7ef7266c | 301 | } |
jplunkett | 0:3f9d7ef7266c | 302 | |
jplunkett | 0:3f9d7ef7266c | 303 | void CameraOV528::_set_baud(uint32_t baud) |
jplunkett | 0:3f9d7ef7266c | 304 | { |
jplunkett | 0:3f9d7ef7266c | 305 | // Baud Rate = 14.7456MHz/(2*(2nd divider +1))/(2*(1st divider+1)) |
jplunkett | 0:3f9d7ef7266c | 306 | uint32_t div2 = 1; |
jplunkett | 0:3f9d7ef7266c | 307 | uint32_t div1 = PIC_CLOCK_HZ / _baud / (2 * (div2 + 1)) / 2 - 1; |
jplunkett | 0:3f9d7ef7266c | 308 | // Assert no rounding errors |
jplunkett | 0:3f9d7ef7266c | 309 | MBED_ASSERT(PIC_CLOCK_HZ / ( 2 * (div2 + 1) ) / ( 2 * (div1 + 1)) == _baud); |
jplunkett | 0:3f9d7ef7266c | 310 | if (!_send_cmd(SET_BAUD_RATE, div1, div2)) { |
jplunkett | 1:06afc809909b | 311 | camera_error("_set_baud failed"); |
jplunkett | 0:3f9d7ef7266c | 312 | } |
jplunkett | 0:3f9d7ef7266c | 313 | _serial.baud(_baud); |
jplunkett | 0:3f9d7ef7266c | 314 | } |
jplunkett | 0:3f9d7ef7266c | 315 | |
jplunkett | 0:3f9d7ef7266c | 316 | void CameraOV528::_set_package_size(uint32_t size) |
jplunkett | 0:3f9d7ef7266c | 317 | { |
jplunkett | 0:3f9d7ef7266c | 318 | camera_printf("_set_package_size(%lu)\r\n", size); |
jplunkett | 0:3f9d7ef7266c | 319 | uint8_t size_low = (size >> 0) & 0xff; |
jplunkett | 0:3f9d7ef7266c | 320 | uint8_t size_high = (size >> 8) & 0xff; |
jplunkett | 0:3f9d7ef7266c | 321 | if (!_send_cmd(SET_PACKAGE_SIZE, 0x08, size_low, size_high, 0)) { |
jplunkett | 1:06afc809909b | 322 | camera_error("_set_package_size failed"); |
jplunkett | 0:3f9d7ef7266c | 323 | } |
jplunkett | 0:3f9d7ef7266c | 324 | } |
jplunkett | 0:3f9d7ef7266c | 325 | |
jplunkett | 0:3f9d7ef7266c | 326 | void CameraOV528::_set_fmt_and_res(Format fmt, Resolution res) |
jplunkett | 0:3f9d7ef7266c | 327 | { |
jplunkett | 0:3f9d7ef7266c | 328 | if (!_send_cmd(INITIAL, 0x00, _format, 0x00, _resolution)) { |
jplunkett | 1:06afc809909b | 329 | camera_error("_set_fmt_and_res failed"); |
jplunkett | 0:3f9d7ef7266c | 330 | } |
jplunkett | 0:3f9d7ef7266c | 331 | } |
jplunkett | 0:3f9d7ef7266c | 332 | |
jplunkett | 1:06afc809909b | 333 | int CameraOV528::_read_picture_block() |
jplunkett | 0:3f9d7ef7266c | 334 | { |
jplunkett | 0:3f9d7ef7266c | 335 | const uint32_t payload_length = picture_buffer_size_limit - PIC_PAYLOAD_OVERHEAD; |
jplunkett | 0:3f9d7ef7266c | 336 | if (picture_data_id >= picture_data_id_count) { |
jplunkett | 0:3f9d7ef7266c | 337 | // Transfer complete |
jplunkett | 1:06afc809909b | 338 | return 0; |
jplunkett | 0:3f9d7ef7266c | 339 | } |
jplunkett | 0:3f9d7ef7266c | 340 | |
jplunkett | 0:3f9d7ef7266c | 341 | // Send an ACK to indicate that the next block id should be sent |
jplunkett | 0:3f9d7ef7266c | 342 | uint8_t id_low = (picture_data_id >> 0) & 0xff; |
jplunkett | 0:3f9d7ef7266c | 343 | uint8_t id_high = (picture_data_id >> 8) & 0xff; |
jplunkett | 0:3f9d7ef7266c | 344 | _send_ack(0x00, 0x00, id_low, id_high); |
jplunkett | 0:3f9d7ef7266c | 345 | |
jplunkett | 0:3f9d7ef7266c | 346 | // Read image data |
jplunkett | 0:3f9d7ef7266c | 347 | memset(picture_buffer, 0, sizeof(picture_buffer)); |
jplunkett | 0:3f9d7ef7266c | 348 | uint32_t size_left = picture_length - payload_length * picture_data_id; |
jplunkett | 0:3f9d7ef7266c | 349 | uint32_t size_to_read = min(size_left, payload_length) + PIC_PAYLOAD_OVERHEAD; |
jplunkett | 0:3f9d7ef7266c | 350 | uint32_t size_read = _read(picture_buffer, size_to_read); |
jplunkett | 0:3f9d7ef7266c | 351 | if (size_read != size_to_read) { |
jplunkett | 1:06afc809909b | 352 | return camera_error("Image data protocol error"); |
jplunkett | 0:3f9d7ef7266c | 353 | } |
jplunkett | 0:3f9d7ef7266c | 354 | |
jplunkett | 0:3f9d7ef7266c | 355 | // Validate checksum |
jplunkett | 0:3f9d7ef7266c | 356 | uint8_t checksum = 0; |
jplunkett | 0:3f9d7ef7266c | 357 | for (uint32_t i = 0; i < size_read - 2; i++) { |
jplunkett | 0:3f9d7ef7266c | 358 | checksum += picture_buffer[i]; |
jplunkett | 0:3f9d7ef7266c | 359 | } |
jplunkett | 0:3f9d7ef7266c | 360 | if (picture_buffer[size_read - 2] != checksum) { |
jplunkett | 1:06afc809909b | 361 | return camera_error("Image data checksum failure"); |
jplunkett | 0:3f9d7ef7266c | 362 | } |
jplunkett | 0:3f9d7ef7266c | 363 | |
jplunkett | 0:3f9d7ef7266c | 364 | // Update buffer information |
jplunkett | 0:3f9d7ef7266c | 365 | picture_buffer_pos = PIC_PAYLOAD_DATA_BEGIN; |
jplunkett | 0:3f9d7ef7266c | 366 | picture_buffer_size = size_read - PIC_PAYLOAD_OVERHEAD; |
jplunkett | 0:3f9d7ef7266c | 367 | |
jplunkett | 0:3f9d7ef7266c | 368 | // If this is the last packet then send the completion ACK |
jplunkett | 0:3f9d7ef7266c | 369 | if (picture_data_id + 1 == picture_data_id_count) { |
jplunkett | 0:3f9d7ef7266c | 370 | _send_ack(0x00, 0x00, 0xF0, 0xF0); |
jplunkett | 0:3f9d7ef7266c | 371 | } |
jplunkett | 0:3f9d7ef7266c | 372 | |
jplunkett | 0:3f9d7ef7266c | 373 | // Increment position |
jplunkett | 0:3f9d7ef7266c | 374 | camera_printf("%lu of %lu\r\n", picture_data_id + 1, picture_data_id_count); |
jplunkett | 0:3f9d7ef7266c | 375 | camera_printf("size left %lu\r\n", |
jplunkett | 0:3f9d7ef7266c | 376 | picture_length - payload_length * picture_data_id + |
jplunkett | 0:3f9d7ef7266c | 377 | PIC_PAYLOAD_OVERHEAD - size_read); |
jplunkett | 0:3f9d7ef7266c | 378 | picture_data_id++; |
jplunkett | 1:06afc809909b | 379 | return 0; |
jplunkett | 0:3f9d7ef7266c | 380 | } |
jplunkett | 0:3f9d7ef7266c | 381 | |
jplunkett | 0:3f9d7ef7266c | 382 | void CameraOV528::_send_ack(uint8_t p1, uint8_t p2, uint8_t p3, uint8_t p4) |
jplunkett | 0:3f9d7ef7266c | 383 | { |
jplunkett | 0:3f9d7ef7266c | 384 | camera_command_t cmd = {0}; |
jplunkett | 0:3f9d7ef7266c | 385 | cmd.header = 0xAA; |
jplunkett | 0:3f9d7ef7266c | 386 | cmd.command = ACK; |
jplunkett | 0:3f9d7ef7266c | 387 | cmd.param[0] = p1; |
jplunkett | 0:3f9d7ef7266c | 388 | cmd.param[1] = p2; |
jplunkett | 0:3f9d7ef7266c | 389 | cmd.param[2] = p3; |
jplunkett | 0:3f9d7ef7266c | 390 | cmd.param[3] = p4; |
jplunkett | 0:3f9d7ef7266c | 391 | _send((uint8_t*)&cmd, COMMAND_LENGTH); |
jplunkett | 0:3f9d7ef7266c | 392 | } |
jplunkett | 0:3f9d7ef7266c | 393 | |
jplunkett | 0:3f9d7ef7266c | 394 | void CameraOV528::_rx_irq(void) |
jplunkett | 0:3f9d7ef7266c | 395 | { |
jplunkett | 0:3f9d7ef7266c | 396 | if(_serial.readable()) { |
jplunkett | 0:3f9d7ef7266c | 397 | |
jplunkett | 0:3f9d7ef7266c | 398 | // Check for overflow |
jplunkett | 0:3f9d7ef7266c | 399 | if (_read_buf_head + 1 == _read_buf_tail) { |
jplunkett | 1:06afc809909b | 400 | camera_error("RX buffer overflow"); |
jplunkett | 0:3f9d7ef7266c | 401 | } |
jplunkett | 0:3f9d7ef7266c | 402 | |
jplunkett | 0:3f9d7ef7266c | 403 | // Add data |
jplunkett | 0:3f9d7ef7266c | 404 | _read_buf[_read_buf_head] = (uint8_t)_serial.getc(); |
jplunkett | 0:3f9d7ef7266c | 405 | _read_buf_head = (_read_buf_head + 1) % sizeof(_read_buf); |
jplunkett | 0:3f9d7ef7266c | 406 | |
jplunkett | 0:3f9d7ef7266c | 407 | // Check if thread should be woken |
jplunkett | 0:3f9d7ef7266c | 408 | if (_read_buf_head == _read_wake_pos) { |
jplunkett | 0:3f9d7ef7266c | 409 | _read_sem.release(); |
jplunkett | 0:3f9d7ef7266c | 410 | _read_wake_pos = READ_WAKE_POS_INVALID; |
jplunkett | 0:3f9d7ef7266c | 411 | } |
jplunkett | 0:3f9d7ef7266c | 412 | } |
jplunkett | 0:3f9d7ef7266c | 413 | } |
jplunkett | 0:3f9d7ef7266c | 414 | |
jplunkett | 0:3f9d7ef7266c | 415 | bool CameraOV528::_send(const uint8_t *data, uint32_t size, uint32_t timeout_ms) |
jplunkett | 0:3f9d7ef7266c | 416 | { |
jplunkett | 0:3f9d7ef7266c | 417 | for (uint32_t i = 0; i < size; i++) { |
jplunkett | 0:3f9d7ef7266c | 418 | _serial.putc(data[i]); |
jplunkett | 0:3f9d7ef7266c | 419 | } |
jplunkett | 0:3f9d7ef7266c | 420 | return true; |
jplunkett | 0:3f9d7ef7266c | 421 | } |
jplunkett | 0:3f9d7ef7266c | 422 | |
jplunkett | 0:3f9d7ef7266c | 423 | uint32_t CameraOV528::_read(uint8_t *data, uint32_t size, uint32_t timeout_ms) |
jplunkett | 0:3f9d7ef7266c | 424 | { |
jplunkett | 0:3f9d7ef7266c | 425 | MBED_ASSERT(size < sizeof(_read_buf)); |
jplunkett | 0:3f9d7ef7266c | 426 | MBED_ASSERT(0 == _read_sem.wait(0)); |
jplunkett | 0:3f9d7ef7266c | 427 | |
jplunkett | 0:3f9d7ef7266c | 428 | core_util_critical_section_enter(); |
jplunkett | 0:3f9d7ef7266c | 429 | |
jplunkett | 0:3f9d7ef7266c | 430 | // Atomically set wakeup condition |
jplunkett | 0:3f9d7ef7266c | 431 | uint32_t size_available = queue_used(_read_buf_head, _read_buf_tail, sizeof(_read_buf)); |
jplunkett | 0:3f9d7ef7266c | 432 | if (size_available >= size) { |
jplunkett | 0:3f9d7ef7266c | 433 | _read_wake_pos = READ_WAKE_POS_INVALID; |
jplunkett | 0:3f9d7ef7266c | 434 | } else { |
jplunkett | 0:3f9d7ef7266c | 435 | _read_wake_pos = (_read_buf_tail + size) % sizeof(_read_buf); |
jplunkett | 0:3f9d7ef7266c | 436 | } |
jplunkett | 0:3f9d7ef7266c | 437 | |
jplunkett | 0:3f9d7ef7266c | 438 | core_util_critical_section_exit(); |
jplunkett | 0:3f9d7ef7266c | 439 | |
jplunkett | 0:3f9d7ef7266c | 440 | // Wait until the requested number of bytes are available |
jplunkett | 0:3f9d7ef7266c | 441 | if (_read_wake_pos != READ_WAKE_POS_INVALID) { |
jplunkett | 0:3f9d7ef7266c | 442 | int32_t tokens = _read_sem.wait(timeout_ms); |
jplunkett | 0:3f9d7ef7266c | 443 | if (tokens <= 0) { |
jplunkett | 0:3f9d7ef7266c | 444 | // Timeout occurred so make sure semaphore is cleared |
jplunkett | 0:3f9d7ef7266c | 445 | _read_wake_pos = READ_WAKE_POS_INVALID; |
jplunkett | 0:3f9d7ef7266c | 446 | _read_sem.wait(0); |
jplunkett | 0:3f9d7ef7266c | 447 | } else { |
jplunkett | 0:3f9d7ef7266c | 448 | // If the semaphore was signaled then the requested number of |
jplunkett | 0:3f9d7ef7266c | 449 | // bytes were read |
jplunkett | 0:3f9d7ef7266c | 450 | MBED_ASSERT(queue_used(_read_buf_head, _read_buf_tail, sizeof(_read_buf)) >= size); |
jplunkett | 0:3f9d7ef7266c | 451 | } |
jplunkett | 0:3f9d7ef7266c | 452 | } |
jplunkett | 0:3f9d7ef7266c | 453 | |
jplunkett | 0:3f9d7ef7266c | 454 | // Copy bytes |
jplunkett | 0:3f9d7ef7266c | 455 | size_available = queue_used(_read_buf_head, _read_buf_tail, sizeof(_read_buf)); |
jplunkett | 0:3f9d7ef7266c | 456 | uint32_t read_size = min(size_available, size); |
jplunkett | 0:3f9d7ef7266c | 457 | for (uint32_t i = 0; i < read_size; i++) { |
jplunkett | 0:3f9d7ef7266c | 458 | data[i] = _read_buf[_read_buf_tail]; |
jplunkett | 0:3f9d7ef7266c | 459 | _read_buf_tail = (_read_buf_tail + 1) % sizeof(_read_buf); |
jplunkett | 0:3f9d7ef7266c | 460 | } |
jplunkett | 0:3f9d7ef7266c | 461 | |
jplunkett | 0:3f9d7ef7266c | 462 | return read_size; |
jplunkett | 0:3f9d7ef7266c | 463 | } |
jplunkett | 0:3f9d7ef7266c | 464 | |
jplunkett | 0:3f9d7ef7266c | 465 | |
jplunkett | 0:3f9d7ef7266c | 466 | void CameraOV528::_flush_rx(void) |
jplunkett | 0:3f9d7ef7266c | 467 | { |
jplunkett | 0:3f9d7ef7266c | 468 | |
jplunkett | 0:3f9d7ef7266c | 469 | core_util_critical_section_enter(); |
jplunkett | 0:3f9d7ef7266c | 470 | _read_buf_head = 0; |
jplunkett | 0:3f9d7ef7266c | 471 | _read_buf_tail = 0; |
jplunkett | 0:3f9d7ef7266c | 472 | _read_wake_pos = 0; |
jplunkett | 0:3f9d7ef7266c | 473 | core_util_critical_section_exit(); |
jplunkett | 0:3f9d7ef7266c | 474 | } |
jplunkett | 0:3f9d7ef7266c | 475 | |
jplunkett | 0:3f9d7ef7266c | 476 | bool CameraOV528::_send_cmd(uint8_t cmd, uint8_t p1, uint8_t p2, uint8_t p3, uint8_t p4) |
jplunkett | 0:3f9d7ef7266c | 477 | { |
jplunkett | 0:3f9d7ef7266c | 478 | _flush_rx(); |
jplunkett | 0:3f9d7ef7266c | 479 | |
jplunkett | 0:3f9d7ef7266c | 480 | camera_command_t command = {0}; |
jplunkett | 0:3f9d7ef7266c | 481 | command.header = 0xAA; |
jplunkett | 0:3f9d7ef7266c | 482 | command.command = cmd; |
jplunkett | 0:3f9d7ef7266c | 483 | command.param[0] = p1; |
jplunkett | 0:3f9d7ef7266c | 484 | command.param[1] = p2; |
jplunkett | 0:3f9d7ef7266c | 485 | command.param[2] = p3; |
jplunkett | 0:3f9d7ef7266c | 486 | command.param[3] = p4; |
jplunkett | 0:3f9d7ef7266c | 487 | _send((uint8_t*)&command, COMMAND_LENGTH); |
jplunkett | 0:3f9d7ef7266c | 488 | |
jplunkett | 0:3f9d7ef7266c | 489 | camera_command_t resp = {0}; |
jplunkett | 0:3f9d7ef7266c | 490 | uint32_t len = _read((uint8_t*)&resp, COMMAND_LENGTH); |
jplunkett | 0:3f9d7ef7266c | 491 | if (COMMAND_LENGTH != len) { |
jplunkett | 0:3f9d7ef7266c | 492 | camera_printf("Wrong command response length %lu\r\n", len); |
jplunkett | 0:3f9d7ef7266c | 493 | } |
jplunkett | 0:3f9d7ef7266c | 494 | if ((resp.header != 0xAA) || (resp.command != ACK) || (resp.param[0] != command.command)) { |
jplunkett | 0:3f9d7ef7266c | 495 | camera_printf("Invalid command: %i, %i, %i\r\n", resp.header, resp.command, resp.param[0]); |
jplunkett | 0:3f9d7ef7266c | 496 | return false; |
jplunkett | 0:3f9d7ef7266c | 497 | } |
jplunkett | 0:3f9d7ef7266c | 498 | return true; |
jplunkett | 0:3f9d7ef7266c | 499 | } |
jplunkett | 0:3f9d7ef7266c | 500 | |
jplunkett | 0:3f9d7ef7266c | 501 | bool CameraOV528::_init_sequence() |
jplunkett | 0:3f9d7ef7266c | 502 | { |
jplunkett | 0:3f9d7ef7266c | 503 | camera_printf("connecting to camera..."); |
jplunkett | 0:3f9d7ef7266c | 504 | bool success = false; |
jplunkett | 0:3f9d7ef7266c | 505 | for (uint32_t i = 0; i < 4; i++) { |
jplunkett | 0:3f9d7ef7266c | 506 | camera_printf("."); |
jplunkett | 0:3f9d7ef7266c | 507 | |
jplunkett | 0:3f9d7ef7266c | 508 | // Send SYNC command repeatedly |
jplunkett | 0:3f9d7ef7266c | 509 | if (!_send_cmd(SYNC, 0, 0, 0, 0)) { |
jplunkett | 0:3f9d7ef7266c | 510 | continue; |
jplunkett | 0:3f9d7ef7266c | 511 | } |
jplunkett | 0:3f9d7ef7266c | 512 | // Device should send back SYNC command |
jplunkett | 0:3f9d7ef7266c | 513 | camera_command_t resp = {0}; |
jplunkett | 0:3f9d7ef7266c | 514 | if (_read((uint8_t*)&resp, COMMAND_LENGTH, 500) != COMMAND_LENGTH) { |
jplunkett | 0:3f9d7ef7266c | 515 | continue; |
jplunkett | 0:3f9d7ef7266c | 516 | } |
jplunkett | 0:3f9d7ef7266c | 517 | if (resp.header != 0xAA) continue; |
jplunkett | 0:3f9d7ef7266c | 518 | if (resp.command != SYNC) continue; |
jplunkett | 0:3f9d7ef7266c | 519 | if (resp.param[0] != 0) continue; |
jplunkett | 0:3f9d7ef7266c | 520 | if (resp.param[1] != 0) continue; |
jplunkett | 0:3f9d7ef7266c | 521 | if (resp.param[2] != 0) continue; |
jplunkett | 0:3f9d7ef7266c | 522 | if (resp.param[3] != 0) continue; |
jplunkett | 0:3f9d7ef7266c | 523 | success = true; |
jplunkett | 0:3f9d7ef7266c | 524 | break; |
jplunkett | 0:3f9d7ef7266c | 525 | } |
jplunkett | 0:3f9d7ef7266c | 526 | return success; |
jplunkett | 0:3f9d7ef7266c | 527 | } |
jplunkett | 0:3f9d7ef7266c | 528 | |
jplunkett | 0:3f9d7ef7266c | 529 | static uint32_t divide_round_up(uint32_t dividen, uint32_t divisor) |
jplunkett | 0:3f9d7ef7266c | 530 | { |
jplunkett | 0:3f9d7ef7266c | 531 | return (dividen + divisor - 1) / divisor; |
jplunkett | 0:3f9d7ef7266c | 532 | } |
jplunkett | 0:3f9d7ef7266c | 533 | |
jplunkett | 0:3f9d7ef7266c | 534 | static uint32_t min(uint32_t value1, uint32_t value2) |
jplunkett | 0:3f9d7ef7266c | 535 | { |
jplunkett | 0:3f9d7ef7266c | 536 | return value1 < value2 ? value1 : value2; |
jplunkett | 0:3f9d7ef7266c | 537 | } |
jplunkett | 0:3f9d7ef7266c | 538 | |
jplunkett | 0:3f9d7ef7266c | 539 | static uint32_t queue_used(uint32_t head, uint32_t tail, uint32_t size) |
jplunkett | 0:3f9d7ef7266c | 540 | { |
jplunkett | 0:3f9d7ef7266c | 541 | if (head < tail) { |
jplunkett | 0:3f9d7ef7266c | 542 | return head + size - tail; |
jplunkett | 0:3f9d7ef7266c | 543 | } else { |
jplunkett | 0:3f9d7ef7266c | 544 | return head - tail; |
jplunkett | 0:3f9d7ef7266c | 545 | } |
jplunkett | 2:7a563410b23d | 546 | } |