blob: 8a8161050c6b0a284b2f0bcaf495ed954752eb53 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/webcam_private/visca_webcam.h"
#include <stddef.h>
#include <stdint.h>
#include <utility>
#include "base/bind.h"
#include "base/location.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/task/post_task.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/service_manager_connection.h"
#include "mojo/public/cpp/bindings/interface_request.h"
#include "services/device/public/mojom/constants.mojom.h"
#include "services/service_manager/public/cpp/connector.h"
using content::BrowserThread;
namespace {
// Message terminator:
const char kViscaTerminator = 0xFF;
// Response types:
const char kViscaResponseNetworkChange = 0x38;
const char kViscaResponseAck = 0x40;
const char kViscaResponseError = 0x60;
// The default pan speed is kMaxPanSpeed /2 and the default tilt speed is
// kMaxTiltSpeed / 2.
const int kMaxPanSpeed = 0x18;
const int kMaxTiltSpeed = 0x14;
const int kDefaultPanSpeed = 0x18 / 2;
const int kDefaultTiltSpeed = 0x14 / 2;
// Pan-Tilt-Zoom movement comands from http://www.manualslib.com/manual/...
// 557364/Cisco-Precisionhd-1080p12x.html?page=31#manual
// Reset the address of each device in the VISCA chain (broadcast). This is used
// when resetting the VISCA network.
const char kSetAddressCommand[] = {0x88, 0x30, 0x01, 0xFF};
// Clear all of the devices, halting any pending commands in the VISCA chain
// (broadcast). This is used when resetting the VISCA network.
const char kClearAllCommand[] = {0x88, 0x01, 0x00, 0x01, 0xFF};
// Command: {0x8X, 0x09, 0x06, 0x12, 0xFF}, X = 1 to 7: target device address.
// Response: {0xY0, 0x50, 0x0p, 0x0q, 0x0r, 0x0s, 0x0t, 0x0u, 0x0v, 0x0w, 0xFF},
// Y = socket number; pqrs: pan position; tuvw: tilt position.
const char kGetPanTiltCommand[] = {0x81, 0x09, 0x06, 0x12, 0xFF};
// Command: {0x8X, 0x01, 0x06, 0x02, 0x0p, 0x0t, 0x0q, 0x0r, 0x0s, 0x0u, 0x0v,
// 0x0w, 0x0y, 0x0z, 0xFF}, X = 1 to 7: target device address; p = pan speed;
// t = tilt speed; qrsu = pan position; vwyz = tilt position.
const char kSetPanTiltCommand[] = {0x81, 0x01, 0x06, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF};
// Command: {0x8X, 0x01, 0x06, 0x05, 0xFF}, X = 1 to 7: target device address.
const char kResetPanTiltCommand[] = {0x81, 0x01, 0x06, 0x05, 0xFF};
// Command: {0x8X, 0x09, 0x04, 0x47, 0xFF}, X = 1 to 7: target device address.
// Response: {0xY0, 0x50, 0x0p, 0x0q, 0x0r, 0x0s, 0xFF}, Y = socket number;
// pqrs: zoom position.
const char kGetZoomCommand[] = {0x81, 0x09, 0x04, 0x47, 0xFF};
// Command: {0x8X, 0x01, 0x04, 0x47, 0x0p, 0x0q, 0x0r, 0x0s, 0xFF}, X = 1 to 7:
// target device address; pqrs: zoom position;
const char kSetZoomCommand[] = {0x81, 0x01, 0x04, 0x47, 0x00,
0x00, 0x00, 0x00, 0xFF};
// Command: {0x8X, 0x01, 0x04, 0x38, 0x02, 0xFF}, X = 1 to 7: target device
// address.
const char kSetAutoFocusCommand[] = {0x81, 0x01, 0x04, 0x38, 0x02, 0xFF};
// Command: {0x8X, 0x01, 0x04, 0x38, 0x03, 0xFF}, X = 1 to 7: target device
// address.
const char kSetManualFocusCommand[] = {0x81, 0x01, 0x04, 0x38, 0x03, 0xFF};
// Command: {0x8X, 0x09, 0x04, 0x48, 0xFF}, X = 1 to 7: target device address.
// Response: {0xY0, 0x50, 0x0p, 0x0q, 0x0r, 0x0s, 0xFF}, Y = socket number;
// pqrs: focus position.
const char kGetFocusCommand[] = {0x81, 0x09, 0x04, 0x48, 0xFF};
// Command: {0x8X, 0x01, 0x04, 0x48, 0x0p, 0x0q, 0x0r, 0x0s, 0xFF}, X = 1 to 7:
// target device address; pqrs: focus position;
const char kSetFocusCommand[] = {0x81, 0x01, 0x04, 0x48, 0x00,
0x00, 0x00, 0x00, 0xFF};
// Command: {0x8X, 0x01, 0x06, 0x01, 0x0p, 0x0t, 0x03, 0x01, 0xFF}, X = 1 to 7:
// target device address; p: pan speed; t: tilt speed.
const char kPTUpCommand[] = {0x81, 0x01, 0x06, 0x01, 0x00,
0x00, 0x03, 0x01, 0xFF};
// Command: {0x8X, 0x01, 0x06, 0x01, 0x0p, 0x0t, 0x03, 0x02, 0xFF}, X = 1 to 7:
// target device address; p: pan speed; t: tilt speed.
const char kPTDownCommand[] = {0x81, 0x01, 0x06, 0x01, 0x00,
0x00, 0x03, 0x02, 0xFF};
// Command: {0x8X, 0x01, 0x06, 0x01, 0x0p, 0x0t, 0x0, 0x03, 0xFF}, X = 1 to 7:
// target device address; p: pan speed; t: tilt speed.
const char kPTLeftCommand[] = {0x81, 0x01, 0x06, 0x01, 0x00,
0x00, 0x01, 0x03, 0xFF};
// Command: {0x8X, 0x01, 0x06, 0x01, 0x0p, 0x0t, 0x02, 0x03, 0xFF}, X = 1 to 7:
// target device address; p: pan speed; t: tilt speed.
const char kPTRightCommand[] = {0x81, 0x01, 0x06, 0x01, 0x00,
0x00, 0x02, 0x03, 0xFF};
// Command: {0x8X, 0x01, 0x06, 0x01, 0x03, 0x03, 0x03, 0x03, 0xFF}, X = 1 to 7:
// target device address.
const char kPTStopCommand[] = {0x81, 0x01, 0x06, 0x01, 0x03,
0x03, 0x03, 0x03, 0xFF};
#define CHAR_VECTOR_FROM_ARRAY(array) \
std::vector<char>(array, array + base::size(array))
int ShiftResponseLowerBits(char c, size_t shift) {
return static_cast<int>(c & 0x0F) << shift;
}
bool CanBuildResponseInt(const std::vector<char>& response,
size_t start_index) {
return response.size() >= start_index + 4;
}
int BuildResponseInt(const std::vector<char>& response, size_t start_index) {
return ShiftResponseLowerBits(response[start_index], 12) +
ShiftResponseLowerBits(response[start_index + 1], 8) +
ShiftResponseLowerBits(response[start_index + 2], 4) +
ShiftResponseLowerBits(response[start_index + 3], 0);
}
void ResponseToCommand(std::vector<char>* command,
size_t start_index,
uint16_t response) {
DCHECK(command);
std::vector<char>& command_ref = *command;
command_ref[start_index] |= ((response >> 12) & 0x0F);
command_ref[start_index + 1] |= ((response >> 8) & 0x0F);
command_ref[start_index + 2] |= ((response >> 4 & 0x0F));
command_ref[start_index + 3] |= (response & 0x0F);
}
int CalculateSpeed(int desired_speed, int max_speed, int default_speed) {
int speed = std::min(desired_speed, max_speed);
return speed > 0 ? speed : default_speed;
}
int GetPositiveValue(int value) {
return value < 0x8000 ? value : value - 0xFFFF;
}
} // namespace
namespace extensions {
ViscaWebcam::ViscaWebcam() : pan_(0), tilt_(0), weak_ptr_factory_(this) {}
ViscaWebcam::~ViscaWebcam() {
}
void ViscaWebcam::Open(const std::string& path,
const std::string& extension_id,
const OpenCompleteCallback& open_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
device::mojom::SerialPortManagerPtr port_manager;
device::mojom::SerialPortPtrInfo port_ptr_info;
DCHECK(content::ServiceManagerConnection::GetForProcess());
content::ServiceManagerConnection::GetForProcess()
->GetConnector()
->BindInterface(device::mojom::kServiceName,
mojo::MakeRequest(&port_manager));
port_manager->GetPort(path, mojo::MakeRequest(&port_ptr_info));
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::IO},
base::Bind(&ViscaWebcam::OpenOnIOThread, weak_ptr_factory_.GetWeakPtr(),
extension_id, base::Passed(&port_ptr_info), open_callback));
}
void ViscaWebcam::OpenOnIOThread(const std::string& extension_id,
device::mojom::SerialPortPtrInfo port_ptr_info,
const OpenCompleteCallback& open_callback) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
api::serial::ConnectionOptions options;
// Set the receive buffer size to receive the response data 1 by 1.
options.buffer_size.reset(new int(1));
options.persistent.reset(new bool(false));
options.bitrate.reset(new int(9600));
options.cts_flow_control.reset(new bool(false));
// Enable send and receive timeout error.
options.receive_timeout.reset(new int(3000));
options.send_timeout.reset(new int(3000));
options.data_bits = api::serial::DATA_BITS_EIGHT;
options.parity_bit = api::serial::PARITY_BIT_NO;
options.stop_bits = api::serial::STOP_BITS_ONE;
serial_connection_.reset(
new SerialConnection(extension_id, std::move(port_ptr_info)));
serial_connection_->Open(
options, base::BindOnce(&ViscaWebcam::OnConnected,
weak_ptr_factory_.GetWeakPtr(), open_callback));
}
void ViscaWebcam::OnConnected(const OpenCompleteCallback& open_callback,
bool success) {
if (!success) {
PostOpenFailureTask(open_callback);
return;
}
Send(CHAR_VECTOR_FROM_ARRAY(kSetAddressCommand),
base::Bind(&ViscaWebcam::OnAddressSetCompleted,
weak_ptr_factory_.GetWeakPtr(), open_callback));
}
void ViscaWebcam::OnAddressSetCompleted(
const OpenCompleteCallback& open_callback,
bool success,
const std::vector<char>& response) {
commands_.pop_front();
if (!success) {
PostOpenFailureTask(open_callback);
return;
}
Send(CHAR_VECTOR_FROM_ARRAY(kClearAllCommand),
base::Bind(&ViscaWebcam::OnClearAllCompleted,
weak_ptr_factory_.GetWeakPtr(), open_callback));
}
void ViscaWebcam::OnClearAllCompleted(const OpenCompleteCallback& open_callback,
bool success,
const std::vector<char>& response) {
commands_.pop_front();
if (!success) {
PostOpenFailureTask(open_callback);
return;
}
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::Bind(open_callback, true));
}
void ViscaWebcam::Send(const std::vector<char>& command,
const CommandCompleteCallback& callback) {
commands_.push_back(std::make_pair(command, callback));
// If this is the only command in the queue, send it now.
if (commands_.size() == 1) {
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::IO},
base::Bind(&ViscaWebcam::SendOnIOThread, weak_ptr_factory_.GetWeakPtr(),
command, callback));
}
}
void ViscaWebcam::SendOnIOThread(const std::vector<char>& data,
const CommandCompleteCallback& callback) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
serial_connection_->Send(
std::vector<uint8_t>(data.begin(), data.end()),
base::Bind(&ViscaWebcam::OnSendCompleted, weak_ptr_factory_.GetWeakPtr(),
callback));
}
void ViscaWebcam::OnSendCompleted(const CommandCompleteCallback& callback,
uint32_t bytes_sent,
api::serial::SendError error) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
// TODO(xdai): Check |bytes_sent|?
if (error == api::serial::SEND_ERROR_NONE) {
ReceiveLoop(callback);
} else {
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::Bind(callback, false, std::vector<char>()));
}
}
void ViscaWebcam::ReceiveLoop(const CommandCompleteCallback& callback) {
serial_connection_->Receive(base::Bind(&ViscaWebcam::OnReceiveCompleted,
weak_ptr_factory_.GetWeakPtr(),
callback));
}
void ViscaWebcam::OnReceiveCompleted(const CommandCompleteCallback& callback,
std::vector<uint8_t> data,
api::serial::ReceiveError error) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
data_buffer_.insert(data_buffer_.end(), data.begin(), data.end());
if (error != api::serial::RECEIVE_ERROR_NONE || data_buffer_.empty()) {
// Clear |data_buffer_|.
std::vector<char> response;
response.swap(data_buffer_);
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::Bind(callback, false, response));
return;
}
// Success case. If waiting for more data, then loop until encounter the
// terminator.
if (data_buffer_.back() != kViscaTerminator) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&ViscaWebcam::ReceiveLoop,
weak_ptr_factory_.GetWeakPtr(), callback));
return;
}
// Success case, and a complete response has been received.
// Clear |data_buffer_|.
std::vector<char> response;
response.swap(data_buffer_);
if (response.size() < 2 ||
(static_cast<int>(response[1]) & 0xF0) == kViscaResponseError) {
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::Bind(callback, false, response));
} else if ((static_cast<int>(response[1]) & 0xF0) != kViscaResponseAck &&
(static_cast<int>(response[1]) & 0xFF) !=
kViscaResponseNetworkChange) {
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::Bind(callback, true, response));
} else {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&ViscaWebcam::ReceiveLoop,
weak_ptr_factory_.GetWeakPtr(), callback));
}
}
void ViscaWebcam::OnCommandCompleted(const SetPTZCompleteCallback& callback,
bool success,
const std::vector<char>& response) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// TODO(xdai): Error handling according to |response|.
callback.Run(success);
ProcessNextCommand();
}
void ViscaWebcam::OnInquiryCompleted(InquiryType type,
const GetPTZCompleteCallback& callback,
bool success,
const std::vector<char>& response) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (!success) {
callback.Run(false, 0 /* value */, 0 /* min_value */, 0 /* max_value */);
ProcessNextCommand();
return;
}
bool is_valid_response = false;
switch (type) {
case INQUIRY_PAN:
is_valid_response = CanBuildResponseInt(response, 2);
break;
case INQUIRY_TILT:
is_valid_response = CanBuildResponseInt(response, 6);
break;
case INQUIRY_ZOOM:
is_valid_response = CanBuildResponseInt(response, 2);
break;
case INQUIRY_FOCUS:
is_valid_response = CanBuildResponseInt(response, 2);
break;
}
if (!is_valid_response) {
callback.Run(false, 0 /* value */, 0 /* min_value */, 0 /* max_value */);
ProcessNextCommand();
return;
}
int value = 0;
switch (type) {
case INQUIRY_PAN:
// See kGetPanTiltCommand for the format of response.
pan_ = BuildResponseInt(response, 2);
value = GetPositiveValue(pan_);
break;
case INQUIRY_TILT:
// See kGetPanTiltCommand for the format of response.
tilt_ = BuildResponseInt(response, 6);
value = GetPositiveValue(tilt_);
break;
case INQUIRY_ZOOM:
// See kGetZoomCommand for the format of response.
value = BuildResponseInt(response, 2);
break;
case INQUIRY_FOCUS:
// See kGetFocusCommand for the format of response.
value = BuildResponseInt(response, 2);
break;
}
// TODO(pbos): Add support for valid ranges.
callback.Run(true, value, 0, 0);
ProcessNextCommand();
}
void ViscaWebcam::ProcessNextCommand() {
commands_.pop_front();
if (commands_.empty())
return;
// If there are pending commands, process the next one.
const std::vector<char> next_command = commands_.front().first;
const CommandCompleteCallback next_callback = commands_.front().second;
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::IO},
base::Bind(&ViscaWebcam::SendOnIOThread, weak_ptr_factory_.GetWeakPtr(),
next_command, next_callback));
}
void ViscaWebcam::PostOpenFailureTask(
const OpenCompleteCallback& open_callback) {
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::Bind(open_callback, false /* success? */));
}
void ViscaWebcam::GetPan(const GetPTZCompleteCallback& callback) {
Send(CHAR_VECTOR_FROM_ARRAY(kGetPanTiltCommand),
base::Bind(&ViscaWebcam::OnInquiryCompleted,
weak_ptr_factory_.GetWeakPtr(), INQUIRY_PAN, callback));
}
void ViscaWebcam::GetTilt(const GetPTZCompleteCallback& callback) {
Send(CHAR_VECTOR_FROM_ARRAY(kGetPanTiltCommand),
base::Bind(&ViscaWebcam::OnInquiryCompleted,
weak_ptr_factory_.GetWeakPtr(), INQUIRY_TILT, callback));
}
void ViscaWebcam::GetZoom(const GetPTZCompleteCallback& callback) {
Send(CHAR_VECTOR_FROM_ARRAY(kGetZoomCommand),
base::Bind(&ViscaWebcam::OnInquiryCompleted,
weak_ptr_factory_.GetWeakPtr(), INQUIRY_ZOOM, callback));
}
void ViscaWebcam::GetFocus(const GetPTZCompleteCallback& callback) {
Send(CHAR_VECTOR_FROM_ARRAY(kGetFocusCommand),
base::Bind(&ViscaWebcam::OnInquiryCompleted,
weak_ptr_factory_.GetWeakPtr(), INQUIRY_FOCUS, callback));
}
void ViscaWebcam::SetPan(int value,
int pan_speed,
const SetPTZCompleteCallback& callback) {
int actual_pan_speed =
CalculateSpeed(pan_speed, kMaxPanSpeed, kDefaultPanSpeed);
pan_ = value;
std::vector<char> command = CHAR_VECTOR_FROM_ARRAY(kSetPanTiltCommand);
command[4] |= actual_pan_speed;
command[5] |= kDefaultTiltSpeed;
ResponseToCommand(&command, 6, static_cast<uint16_t>(pan_));
ResponseToCommand(&command, 10, static_cast<uint16_t>(tilt_));
Send(command, base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ViscaWebcam::SetTilt(int value,
int tilt_speed,
const SetPTZCompleteCallback& callback) {
int actual_tilt_speed =
CalculateSpeed(tilt_speed, kMaxTiltSpeed, kDefaultTiltSpeed);
tilt_ = value;
std::vector<char> command = CHAR_VECTOR_FROM_ARRAY(kSetPanTiltCommand);
command[4] |= kDefaultPanSpeed;
command[5] |= actual_tilt_speed;
ResponseToCommand(&command, 6, static_cast<uint16_t>(pan_));
ResponseToCommand(&command, 10, static_cast<uint16_t>(tilt_));
Send(command, base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ViscaWebcam::SetZoom(int value, const SetPTZCompleteCallback& callback) {
int actual_value = std::max(value, 0);
std::vector<char> command = CHAR_VECTOR_FROM_ARRAY(kSetZoomCommand);
ResponseToCommand(&command, 4, actual_value);
Send(command, base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ViscaWebcam::SetFocus(int value, const SetPTZCompleteCallback& callback) {
int actual_value = std::max(value, 0);
std::vector<char> command = CHAR_VECTOR_FROM_ARRAY(kSetFocusCommand);
ResponseToCommand(&command, 4, actual_value);
Send(command, base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ViscaWebcam::SetAutofocusState(AutofocusState state,
const SetPTZCompleteCallback& callback) {
std::vector<char> command;
if (state == AUTOFOCUS_ON) {
command = CHAR_VECTOR_FROM_ARRAY(kSetAutoFocusCommand);
} else {
command = CHAR_VECTOR_FROM_ARRAY(kSetManualFocusCommand);
}
Send(command, base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ViscaWebcam::SetPanDirection(PanDirection direction,
int pan_speed,
const SetPTZCompleteCallback& callback) {
int actual_pan_speed =
CalculateSpeed(pan_speed, kMaxPanSpeed, kDefaultPanSpeed);
std::vector<char> command = CHAR_VECTOR_FROM_ARRAY(kPTStopCommand);
switch (direction) {
case PAN_STOP:
break;
case PAN_RIGHT:
command = CHAR_VECTOR_FROM_ARRAY(kPTRightCommand);
command[4] |= actual_pan_speed;
command[5] |= kDefaultTiltSpeed;
break;
case PAN_LEFT:
command = CHAR_VECTOR_FROM_ARRAY(kPTLeftCommand);
command[4] |= actual_pan_speed;
command[5] |= kDefaultTiltSpeed;
break;
}
Send(command, base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ViscaWebcam::SetTiltDirection(TiltDirection direction,
int tilt_speed,
const SetPTZCompleteCallback& callback) {
int actual_tilt_speed =
CalculateSpeed(tilt_speed, kMaxTiltSpeed, kDefaultTiltSpeed);
std::vector<char> command = CHAR_VECTOR_FROM_ARRAY(kPTStopCommand);
switch (direction) {
case TILT_STOP:
break;
case TILT_UP:
command = CHAR_VECTOR_FROM_ARRAY(kPTUpCommand);
command[4] |= kDefaultPanSpeed;
command[5] |= actual_tilt_speed;
break;
case TILT_DOWN:
command = CHAR_VECTOR_FROM_ARRAY(kPTDownCommand);
command[4] |= kDefaultPanSpeed;
command[5] |= actual_tilt_speed;
break;
}
Send(command, base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void ViscaWebcam::Reset(bool pan,
bool tilt,
bool zoom,
const SetPTZCompleteCallback& callback) {
// pan and tilt are always reset together in Visca Webcams.
if (pan || tilt) {
Send(CHAR_VECTOR_FROM_ARRAY(kResetPanTiltCommand),
base::Bind(&ViscaWebcam::OnCommandCompleted,
weak_ptr_factory_.GetWeakPtr(), callback));
}
if (zoom) {
// Set the default zoom value to 100 to be consistent with V4l2 webcam.
const int kDefaultZoom = 100;
SetZoom(kDefaultZoom, callback);
}
}
void ViscaWebcam::OpenForTesting(
std::unique_ptr<SerialConnection> serial_connection) {
serial_connection_ = std::move(serial_connection);
}
SerialConnection* ViscaWebcam::GetSerialConnectionForTesting() {
return serial_connection_.get();
}
} // namespace extensions