diff --git a/contrib/ci/build_windows.sh b/contrib/ci/build_windows.sh
index 92fad1431..1ea32dff8 100755
--- a/contrib/ci/build_windows.sh
+++ b/contrib/ci/build_windows.sh
@@ -7,6 +7,13 @@ export DESTDIR=${root}/dist
build=$root/build-win32
rm -rf $DESTDIR $build
+# For logitech bulk controller being disabled (-Dplugin_logitech_bulkcontroller=false):
+# See https://bugzilla.redhat.com/show_bug.cgi?id=1991749
+# When fixed need to do the following to enable:
+# 1. need to add mingw64-protobuf mingw64-protobuf-tools to CI build deps
+# 2. add protoc = /path/to/protoc-c.exe in mingw64.cross
+# 3. Only enable when not a tagged release (Unsupported by Logitech)
+
#build
mkdir -p $build $DESTDIR && cd $build
meson .. \
@@ -22,6 +29,7 @@ meson .. \
-Dplugin_redfish=false \
-Dplugin_altos=false \
-Dplugin_dell=false \
+ -Dplugin_logitech_bulkcontroller=false \
-Dplugin_nvme=false \
-Dplugin_parade_lspcon=false \
-Dplugin_realtek_mst=false \
diff --git a/contrib/ci/dependencies.xml b/contrib/ci/dependencies.xml
index dd15781d4..deca01370 100644
--- a/contrib/ci/dependencies.xml
+++ b/contrib/ci/dependencies.xml
@@ -1553,4 +1553,41 @@
ShellCheck
+
+
+ protobuf-c
+
+
+
+
+
+
+
+
+
+
+
+ protobuf-c-devel
+
+
+ protobuf-c-devel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ protobuf
+
+
diff --git a/contrib/freebsd/Makefile b/contrib/freebsd/Makefile
index 85980f8c2..369263d56 100644
--- a/contrib/freebsd/Makefile
+++ b/contrib/freebsd/Makefile
@@ -26,6 +26,7 @@ LIB_DEPENDS= libcurl.so:ftp/curl \
libgusb.so:devel/libgusb \
libjcat.so:textproc/libjcat \
libjson-glib-1.0.so:devel/json-glib \
+ libprotobuf-c.so:devel/protobuf-c \
libxmlb.so:textproc/libxmlb \
libefiboot.so:devel/gnu-efi
diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in
index ba3fa6e53..d9555ee4c 100644
--- a/contrib/fwupd.spec.in
+++ b/contrib/fwupd.spec.in
@@ -481,6 +481,7 @@ done
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_uefi_recovery.so
%endif
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_logind.so
+%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_logitech_bulkcontroller.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_logitech_hidpp.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_upower.so
%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_vli.so
diff --git a/meson.build b/meson.build
index 84702a981..2ac22cd44 100644
--- a/meson.build
+++ b/meson.build
@@ -544,6 +544,12 @@ if get_option('libarchive')
plugin_deps += libarchive
endif
+if get_option('plugin_logitech_bulkcontroller')
+ protobufc = dependency('libprotobuf-c')
+ protoc = find_program('protoc', 'protoc-c')
+ plugin_deps += protobufc
+endif
+
root_incdir = include_directories('.')
if get_option('docs') == 'gtkdoc'
diff --git a/meson_options.txt b/meson_options.txt
index 4e7afb18b..639724c22 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -17,6 +17,7 @@ option('plugin_amt', type : 'boolean', value : true, description : 'enable Intel
option('plugin_dell', type : 'boolean', value : true, description : 'enable Dell-specific support')
option('plugin_dummy', type : 'boolean', value : false, description : 'enable the dummy device')
option('plugin_emmc', type : 'boolean', value : true, description : 'enable eMMC support')
+option('plugin_logitech_bulkcontroller', type : 'boolean', value : true, description : 'enable Logitech bulk controller support')
option('plugin_parade_lspcon', type : 'boolean', value : true, description : 'enable Parade LSPCON support')
option('plugin_realtek_mst', type : 'boolean', value : true, description : 'enable Realtek MST hub support')
option('plugin_synaptics_mst', type: 'boolean', value: true, description : 'enable Synaptics MST hub support')
diff --git a/plugins/logitech-bulkcontroller/README.md b/plugins/logitech-bulkcontroller/README.md
new file mode 100644
index 000000000..67df69a59
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/README.md
@@ -0,0 +1,40 @@
+# Logitech Video Collaboration
+
+## Introduction
+
+This plugin can upgrade the firmware on Logitech Video Collaboration products (Rally Bar and Rally Bar Mini), using USB bulk transfer.
+
+## Firmware Format
+
+The daemon will decompress the cabinet archive and extract a firmware blob in
+a packed binary file format.
+
+This plugin supports the following protocol ID:
+
+* com.logitech.vc.proto
+
+## GUID Generation
+
+These devices use the standard USB DeviceInstanceId values, e.g.
+
+* `USB\VID_046D&PID_089B`
+* `USB\VID_046D&PID_08D3`
+
+## Quirk Use
+
+This plugin uses the following plugin-specific quirks:
+
+## Update Behavior
+
+The peripheral firmware is deployed when the device is in normal runtime mode,
+and the device will reset when the new firmware has been written.
+
+## Design Notes
+
+## Vendor ID Security
+
+The vendor ID is set from the USB vendor, in this instance set to `USB:0x046D`
+
+## External Interface Access
+
+This plugin requires read/write access to `/dev/bus/usb`.
diff --git a/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.c b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.c
new file mode 100644
index 000000000..b5d48fa15
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.c
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include
+
+#include "fu-logitech-bulkcontroller-common.h"
+#include "usb_msg.pb-c.h"
+
+static void
+proto_manager_set_header(Logi__Device__Proto__Header *header_msg)
+{
+ gint64 timestamp_tv;
+
+ g_return_if_fail(header_msg != NULL);
+
+ timestamp_tv = g_get_real_time();
+ header_msg->id = g_uuid_string_random();
+ header_msg->timestamp = g_strdup_printf("%" G_GINT64_FORMAT, timestamp_tv / 1000);
+}
+
+GByteArray *
+proto_manager_generate_get_device_info_request(void)
+{
+ GByteArray *buf = g_byte_array_new();
+ Logi__Device__Proto__Header header_msg = LOGI__DEVICE__PROTO__HEADER__INIT;
+ Logi__Device__Proto__GetDeviceInfoRequest get_deviceinfo_msg =
+ LOGI__DEVICE__PROTO__GET_DEVICE_INFO_REQUEST__INIT;
+ Logi__Device__Proto__Request request_msg = {
+ PROTOBUF_C_MESSAGE_INIT(&logi__device__proto__request__descriptor),
+ LOGI__DEVICE__PROTO__REQUEST__PAYLOAD_GET_DEVICE_INFO_REQUEST,
+ {&get_deviceinfo_msg}};
+ Logi__Device__Proto__UsbMsg usb_msg = LOGI__DEVICE__PROTO__USB_MSG__INIT;
+
+ proto_manager_set_header(&header_msg);
+ usb_msg.header = &header_msg;
+ usb_msg.message_case = LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_REQUEST;
+ usb_msg.request = &request_msg;
+
+ fu_byte_array_set_size(buf, logi__device__proto__usb_msg__get_packed_size(&usb_msg));
+ logi__device__proto__usb_msg__pack(&usb_msg, (unsigned char *)buf->data);
+ return buf;
+}
+
+GByteArray *
+proto_manager_generate_transition_to_device_mode_request(void)
+{
+ GByteArray *buf = g_byte_array_new();
+ Logi__Device__Proto__Header header_msg = LOGI__DEVICE__PROTO__HEADER__INIT;
+ Logi__Device__Proto__TransitionToDeviceModeRequest transition_to_device_mode_msg =
+ LOGI__DEVICE__PROTO__TRANSITION_TO_DEVICE_MODE_REQUEST__INIT;
+ Logi__Device__Proto__Request request_msg = {
+ PROTOBUF_C_MESSAGE_INIT(&logi__device__proto__request__descriptor),
+ LOGI__DEVICE__PROTO__REQUEST__PAYLOAD_TRANSITION_TO_DEVICEMODE_REQUEST,
+ {(Logi__Device__Proto__GetDeviceInfoRequest *)&transition_to_device_mode_msg}};
+ Logi__Device__Proto__UsbMsg usb_msg = LOGI__DEVICE__PROTO__USB_MSG__INIT;
+
+ proto_manager_set_header(&header_msg);
+ usb_msg.header = &header_msg;
+ usb_msg.message_case = LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_REQUEST;
+ usb_msg.request = &request_msg;
+
+ fu_byte_array_set_size(buf, logi__device__proto__usb_msg__get_packed_size(&usb_msg));
+ logi__device__proto__usb_msg__pack(&usb_msg, (unsigned char *)buf->data);
+ return buf;
+}
+
+GByteArray *
+proto_manager_decode_message(const guint8 *data,
+ guint32 len,
+ FuLogitechBulkcontrollerProtoId *proto_id,
+ GError **error)
+{
+ g_autoptr(GByteArray) buf_decoded = g_byte_array_new();
+
+ Logi__Device__Proto__UsbMsg *usb_msg =
+ logi__device__proto__usb_msg__unpack(NULL, len, (const unsigned char *)data);
+ if (usb_msg == NULL) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "unable to unpack data");
+ return NULL;
+ }
+
+ switch (usb_msg->message_case) {
+ case LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_ACK:
+ *proto_id = kProtoId_Ack;
+ break;
+ case LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_RESPONSE:
+ if (!usb_msg->response) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "no USB response");
+ return NULL;
+ }
+ switch (usb_msg->response->payload_case) {
+ case LOGI__DEVICE__PROTO__RESPONSE__PAYLOAD_GET_DEVICE_INFO_RESPONSE:
+ if (usb_msg->response->get_device_info_response) {
+ const gchar *tmp =
+ usb_msg->response->get_device_info_response->payload;
+ *proto_id = kProtoId_GetDeviceInfoResponse;
+ if (tmp != NULL)
+ g_byte_array_append(buf_decoded,
+ (const guint8 *)tmp,
+ strlen(tmp));
+ }
+ break;
+ case LOGI__DEVICE__PROTO__RESPONSE__PAYLOAD_TRANSITION_TO_DEVICEMODE_RESPONSE:
+ if (usb_msg->response->transition_to_devicemode_response) {
+ *proto_id = kProtoId_TransitionToDeviceModeResponse;
+ fu_byte_array_append_uint8(
+ buf_decoded,
+ usb_msg->response->transition_to_devicemode_response->success);
+ }
+ break;
+ default:
+ break;
+ };
+ break;
+ case LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_EVENT:
+ if (!usb_msg->response) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "no USB event");
+ return NULL;
+ }
+ switch (usb_msg->event->payload_case) {
+ case LOGI__DEVICE__PROTO__EVENT__PAYLOAD_KONG_EVENT:
+ if (usb_msg->event->kong_event) {
+ const gchar *tmp = usb_msg->event->kong_event->mqtt_event;
+ *proto_id = kProtoId_KongEvent;
+ if (tmp != NULL)
+ g_byte_array_append(buf_decoded,
+ (const guint8 *)tmp,
+ strlen(tmp));
+ }
+ break;
+ case LOGI__DEVICE__PROTO__EVENT__PAYLOAD_HANDSHAKE_EVENT:
+ if (usb_msg->event->handshake_event) {
+ *proto_id = kProtoId_HandshakeEvent;
+ }
+ break;
+ case LOGI__DEVICE__PROTO__EVENT__PAYLOAD_CRASH_DUMP_AVAILABLE_EVENT:
+ *proto_id = kProtoId_CrashDumpAvailableEvent;
+ break;
+ default:
+ break;
+ };
+ break;
+ default:
+ break;
+ };
+ logi__device__proto__usb_msg__free_unpacked(usb_msg, NULL);
+ return g_steal_pointer(&buf_decoded);
+}
+
+const gchar *
+fu_logitech_bulkcontroller_device_status_to_string(FuLogitechBulkcontrollerDeviceStatus status)
+{
+ if (status == kDeviceStateUnknown)
+ return "Unknown";
+ if (status == kDeviceStateOffline)
+ return "Offline";
+ if (status == kDeviceStateOnline)
+ return "Online";
+ if (status == kDeviceStateIdle)
+ return "Idle";
+ if (status == kDeviceStateInUse)
+ return "InUse";
+ if (status == kDeviceStateAudioOnly)
+ return "AudioOnly";
+ if (status == kDeviceStateEnumerating)
+ return "Enumerating";
+ return NULL;
+}
+
+const gchar *
+fu_logitech_bulkcontroller_device_update_state_to_string(
+ FuLogitechBulkcontrollerDeviceUpdateState update_state)
+{
+ if (update_state == kUpdateStateUnknown)
+ return "Unknown";
+ if (update_state == kUpdateStateCurrent)
+ return "Current";
+ if (update_state == kUpdateStateAvailable)
+ return "Available";
+ if (update_state == kUpdateStateStarting)
+ return "Starting";
+ if (update_state == kUpdateStateDownloading)
+ return "Downloading";
+ if (update_state == kUpdateStateReady)
+ return "Ready";
+ if (update_state == kUpdateStateUpdating)
+ return "Updating";
+ if (update_state == kUpdateStateScheduled)
+ return "Scheduled";
+ if (update_state == kUpdateStateError)
+ return "Error";
+ return NULL;
+}
diff --git a/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.h b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.h
new file mode 100644
index 000000000..fa397e77a
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include
+
+#include "usb_msg.pb-c.h"
+
+typedef enum {
+ kDeviceStateUnknown = -1,
+ kDeviceStateOffline,
+ kDeviceStateOnline,
+ kDeviceStateIdle,
+ kDeviceStateInUse,
+ kDeviceStateAudioOnly,
+ kDeviceStateEnumerating
+} FuLogitechBulkcontrollerDeviceStatus;
+
+typedef enum {
+ kUpdateStateUnknown = -1,
+ kUpdateStateCurrent,
+ kUpdateStateAvailable,
+ kUpdateStateStarting = 3,
+ kUpdateStateDownloading,
+ kUpdateStateReady,
+ kUpdateStateUpdating,
+ kUpdateStateScheduled,
+ kUpdateStateError
+} FuLogitechBulkcontrollerDeviceUpdateState;
+
+typedef enum {
+ kProtoId_UnknownId,
+ kProtoId_GetDeviceInfoResponse,
+ kProtoId_TransitionToDeviceModeResponse,
+ kProtoId_Ack,
+ kProtoId_KongEvent,
+ kProtoId_HandshakeEvent,
+ kProtoId_CrashDumpAvailableEvent
+} FuLogitechBulkcontrollerProtoId;
+
+const gchar *
+fu_logitech_bulkcontroller_device_status_to_string(FuLogitechBulkcontrollerDeviceStatus status);
+const gchar *
+fu_logitech_bulkcontroller_device_update_state_to_string(
+ FuLogitechBulkcontrollerDeviceUpdateState update_state);
+GByteArray *
+proto_manager_generate_get_device_info_request(void);
+GByteArray *
+proto_manager_generate_transition_to_device_mode_request(void);
+GByteArray *
+proto_manager_decode_message(const guint8 *data,
+ guint32 len,
+ FuLogitechBulkcontrollerProtoId *proto_id,
+ GError **error);
diff --git a/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.c b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.c
new file mode 100644
index 000000000..b4fc61c1a
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.c
@@ -0,0 +1,704 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * Copyright (C) 2021 Richard Hughes
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+
+#include "fu-logitech-bulkcontroller-common.h"
+#include "fu-logitech-bulkcontroller-device.h"
+
+/* SYNC interface follows TLSV (Type, Length, SequenceID, Value) protocol */
+/* UPD interface follows TLV (Type, Length, Value) protocol */
+/* Payload size limited to 8k for both interfaces */
+#define UPD_PACKET_HEADER_SIZE (2 * sizeof(guint32))
+#define SYNC_PACKET_HEADER_SIZE (3 * sizeof(guint32))
+#define WRITE_TIME_OUT 100
+#define HASH_TIMEOUT 30000
+#define MAX_DATA_SIZE 8192 /* 8k */
+#define PAYLOAD_SIZE MAX_DATA_SIZE - UPD_PACKET_HEADER_SIZE
+#define UPD_INTERFACE_SUBPROTOCOL_ID 117
+#define SYNC_INTERFACE_SUBPROTOCOL_ID 118
+#define BULK_TRANSFER_TIMEOUT 1000
+#define HASH_VALUE_SIZE 16
+#define LENGTH_OFFSET 0x4
+#define COMMAND_OFFSET 0x0
+#define SYNC_ACK_PAYLOAD_LENGTH 5
+
+enum { SHA_256, SHA_512, MD5 };
+
+enum { EP_OUT, EP_IN, EP_LAST };
+
+enum { BULK_INTERFACE_UPD, BULK_INTERFACE_SYNC };
+
+typedef enum {
+ CMD_CHECK_BUFFERSIZE = 0xCC00,
+ CMD_INIT = 0xCC01,
+ CMD_START_TRANSFER = 0xCC02,
+ CMD_DATA_TRANSFER = 0xCC03,
+ CMD_END_TRANSFER = 0xCC04,
+ CMD_UNINIT = 0xCC05,
+ CMD_BUFFER_READ = 0xCC06,
+ CMD_BUFFER_WRITE = 0xCC07,
+ CMD_UNINIT_BUFFER = 0xCC08,
+ CMD_ACK = 0xFF01,
+ CMD_TIMEOUT = 0xFF02,
+ CMD_NACK = 0xFF03
+} UsbCommands;
+
+struct _FuLogitechBulkcontrollerDevice {
+ FuUsbDevice parent_instance;
+ guint sync_ep[EP_LAST];
+ guint update_ep[EP_LAST];
+ guint sync_iface;
+ guint update_iface;
+ FuLogitechBulkcontrollerDeviceStatus status;
+ FuLogitechBulkcontrollerDeviceUpdateState update_status;
+};
+
+G_DEFINE_TYPE(FuLogitechBulkcontrollerDevice, fu_logitech_bulkcontroller_device, FU_TYPE_USB_DEVICE)
+
+static void
+fu_logitech_bulkcontroller_device_to_string(FuDevice *device, guint idt, GString *str)
+{
+ FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device);
+ fu_common_string_append_kx(str, idt, "SyncIface", self->sync_iface);
+ fu_common_string_append_kx(str, idt, "UpdateIface", self->update_iface);
+ fu_common_string_append_kv(
+ str,
+ idt,
+ "Status",
+ fu_logitech_bulkcontroller_device_status_to_string(self->status));
+ fu_common_string_append_kv(
+ str,
+ idt,
+ "UpdateState",
+ fu_logitech_bulkcontroller_device_update_state_to_string(self->update_status));
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_probe(FuDevice *device, GError **error)
+{
+ FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device);
+ g_autoptr(GPtrArray) intfs = NULL;
+
+ intfs = g_usb_device_get_interfaces(fu_usb_device_get_dev(FU_USB_DEVICE(self)), error);
+ if (intfs == NULL)
+ return FALSE;
+ for (guint i = 0; i < intfs->len; i++) {
+ GUsbInterface *intf = g_ptr_array_index(intfs, i);
+ if (g_usb_interface_get_class(intf) == G_USB_DEVICE_CLASS_VENDOR_SPECIFIC &&
+ g_usb_interface_get_protocol(intf) == 0x1) {
+ if (g_usb_interface_get_subclass(intf) == SYNC_INTERFACE_SUBPROTOCOL_ID) {
+ g_autoptr(GPtrArray) endpoints =
+ g_usb_interface_get_endpoints(intf);
+ self->sync_iface = g_usb_interface_get_number(intf);
+ if (endpoints == NULL)
+ continue;
+ for (guint j = 0; j < endpoints->len; j++) {
+ GUsbEndpoint *ep = g_ptr_array_index(endpoints, j);
+ if (j == EP_OUT)
+ self->sync_ep[EP_OUT] =
+ g_usb_endpoint_get_address(ep);
+ else
+ self->sync_ep[EP_IN] =
+ g_usb_endpoint_get_address(ep);
+ }
+ } else if (g_usb_interface_get_subclass(intf) ==
+ UPD_INTERFACE_SUBPROTOCOL_ID) {
+ g_autoptr(GPtrArray) endpoints =
+ g_usb_interface_get_endpoints(intf);
+ self->sync_iface = g_usb_interface_get_number(intf);
+ if (endpoints == NULL)
+ continue;
+ for (guint j = 0; j < endpoints->len; j++) {
+ GUsbEndpoint *ep = g_ptr_array_index(endpoints, j);
+ if (j == EP_OUT)
+ self->update_ep[EP_OUT] =
+ g_usb_endpoint_get_address(ep);
+ else
+ self->update_ep[EP_IN] =
+ g_usb_endpoint_get_address(ep);
+ }
+ }
+ }
+ }
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_send(FuLogitechBulkcontrollerDevice *self,
+ GByteArray *buf,
+ gint interface_id,
+ GError **error)
+{
+ gsize transferred = 0;
+ gint ep;
+ GCancellable *cancellable = NULL;
+ g_return_val_if_fail(buf != NULL, FALSE);
+
+ if (interface_id == BULK_INTERFACE_SYNC) {
+ ep = self->sync_ep[EP_OUT];
+ } else if (interface_id == BULK_INTERFACE_UPD) {
+ ep = self->update_ep[EP_OUT];
+ } else {
+ g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "interface is invalid");
+ return FALSE;
+ }
+ if (!g_usb_device_bulk_transfer(fu_usb_device_get_dev(FU_USB_DEVICE(self)),
+ ep,
+ (guint8 *)buf->data,
+ buf->len,
+ &transferred,
+ WRITE_TIME_OUT,
+ cancellable,
+ error)) {
+ g_prefix_error(error, "bulk transfer failed: ");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_recv(FuLogitechBulkcontrollerDevice *self,
+ GByteArray *buf,
+ gint interface_id,
+ guint timeout,
+ GError **error)
+{
+ gsize received_length = 0;
+ gint ep;
+
+ g_return_val_if_fail(buf != NULL, FALSE);
+
+ if (interface_id == BULK_INTERFACE_SYNC) {
+ ep = self->sync_ep[EP_IN];
+ } else if (interface_id == BULK_INTERFACE_UPD) {
+ ep = self->update_ep[EP_IN];
+ } else {
+ g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "interface is invalid");
+ return FALSE;
+ }
+ if (!g_usb_device_bulk_transfer(fu_usb_device_get_dev(FU_USB_DEVICE(self)),
+ ep,
+ buf->data,
+ buf->len,
+ &received_length,
+ timeout,
+ NULL,
+ error)) {
+ g_prefix_error(error, "bulk transfer failed: ");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_send_upd_cmd(FuLogitechBulkcontrollerDevice *self,
+ guint32 cmd,
+ GByteArray *buf,
+ GError **error)
+{
+ guint32 cmd_tmp = 0x0;
+ guint timeout = BULK_TRANSFER_TIMEOUT;
+ g_autoptr(GByteArray) buf_pkt = g_byte_array_new();
+ g_autoptr(GByteArray) buf_ack = g_byte_array_new();
+
+ fu_byte_array_append_uint32(buf_pkt, cmd, G_LITTLE_ENDIAN); /* Type(T) : Command type */
+ fu_byte_array_append_uint32(buf_pkt,
+ buf != NULL ? buf->len : 0,
+ G_LITTLE_ENDIAN); /*Length(L) : Length of payload */
+ if (buf != NULL) {
+ g_byte_array_append(buf_pkt,
+ buf->data,
+ buf->len); /* Value(V) : Actual payload data */
+ }
+ if (!fu_logitech_bulkcontroller_device_send(self, buf_pkt, BULK_INTERFACE_UPD, error))
+ return FALSE;
+
+ /* receiving INIT ACK */
+ fu_byte_array_set_size(buf_ack, MAX_DATA_SIZE);
+
+ /* Extending the bulk transfer timeout value, as android device takes some time to
+ calculate Hash and respond */
+ if (CMD_END_TRANSFER == cmd)
+ timeout = HASH_TIMEOUT;
+
+ if (!fu_logitech_bulkcontroller_device_recv(self,
+ buf_ack,
+ BULK_INTERFACE_UPD,
+ timeout,
+ error))
+ return FALSE;
+
+ if (!fu_common_read_uint32_safe(buf_ack->data,
+ buf_ack->len,
+ COMMAND_OFFSET,
+ &cmd_tmp,
+ G_LITTLE_ENDIAN,
+ error))
+ return FALSE;
+ if (cmd_tmp != CMD_ACK) {
+ g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "not CMD_ACK, got %x", cmd);
+ return FALSE;
+ }
+ if (!fu_common_read_uint32_safe(buf_ack->data,
+ buf_ack->len,
+ UPD_PACKET_HEADER_SIZE,
+ &cmd_tmp,
+ G_LITTLE_ENDIAN,
+ error))
+ return FALSE;
+ if (cmd_tmp != cmd) {
+ g_set_error(error,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "invalid upd message received, expected %x, got %x",
+ cmd,
+ cmd_tmp);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_send_sync_cmd(FuLogitechBulkcontrollerDevice *self,
+ guint32 cmd,
+ GByteArray *buf,
+ GError **error)
+{
+ guint32 cmd_tmp = 0x0;
+ guint64 cmd_tmp_64 = 0x0;
+ guint32 verify_cmd = 0x0;
+ gchar ack_payload[SYNC_ACK_PAYLOAD_LENGTH] = {0x0};
+ g_autoptr(GByteArray) buf_pkt = g_byte_array_new();
+ g_autoptr(GByteArray) buf_ack = g_byte_array_new();
+
+ fu_byte_array_append_uint32(buf_pkt, cmd, G_LITTLE_ENDIAN); /* Type(T) : Command type */
+ fu_byte_array_append_uint32(buf_pkt,
+ buf != NULL ? buf->len : 0,
+ G_LITTLE_ENDIAN); /*Length(L) : Length of payload */
+ fu_byte_array_append_uint32(buf_pkt,
+ g_random_int_range(0, G_MAXUINT16),
+ G_LITTLE_ENDIAN); /*Sequence(S) : Sequence ID of the data */
+ if (buf != NULL) {
+ g_byte_array_append(buf_pkt,
+ buf->data,
+ buf->len); /* Value(V) : Actual payload data */
+ }
+ if (!fu_logitech_bulkcontroller_device_send(self, buf_pkt, BULK_INTERFACE_SYNC, error))
+ return FALSE;
+
+ /* receiving ACK */
+ fu_byte_array_set_size(buf_ack, MAX_DATA_SIZE);
+ if (!fu_logitech_bulkcontroller_device_recv(self,
+ buf_ack,
+ BULK_INTERFACE_SYNC,
+ BULK_TRANSFER_TIMEOUT,
+ error))
+ return FALSE;
+ if (!fu_common_read_uint32_safe(buf_ack->data,
+ buf_ack->len,
+ COMMAND_OFFSET,
+ &cmd_tmp,
+ G_LITTLE_ENDIAN,
+ error))
+ return FALSE;
+ if (cmd_tmp != CMD_ACK) {
+ g_set_error(error,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "not sync CMD_ACK, got %x",
+ cmd_tmp);
+ return FALSE;
+ }
+ if (!fu_common_read_uint64_safe(buf_ack->data,
+ buf_ack->len,
+ SYNC_PACKET_HEADER_SIZE,
+ &cmd_tmp_64,
+ G_LITTLE_ENDIAN,
+ error))
+ return FALSE;
+ if (!fu_memcpy_safe((guint8 *)ack_payload,
+ sizeof(ack_payload),
+ 0x0,
+ (guint8 *)&cmd_tmp_64,
+ sizeof(cmd_tmp_64),
+ 0x0,
+ SYNC_ACK_PAYLOAD_LENGTH,
+ error)) {
+ g_prefix_error(error, "failed to retrieve payload data: ");
+ return FALSE;
+ }
+ verify_cmd = fu_common_strtoull(ack_payload);
+ if (verify_cmd != cmd) {
+ g_set_error(error,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "invalid sync message payload received, expected %x, got %x",
+ cmd,
+ verify_cmd);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_recv_sync_cmd(FuLogitechBulkcontrollerDevice *self,
+ guint32 cmd,
+ GByteArray *buf,
+ GError **error)
+{
+ guint32 cmd_tmp = 0x0;
+ guint32 response_length = 0;
+ g_autoptr(GByteArray) buf_pkt = g_byte_array_new();
+ g_autoptr(GByteArray) buf_ack = g_byte_array_new();
+
+ fu_byte_array_set_size(buf_pkt, MAX_DATA_SIZE);
+ if (!fu_logitech_bulkcontroller_device_recv(self,
+ buf_pkt,
+ BULK_INTERFACE_SYNC,
+ BULK_TRANSFER_TIMEOUT,
+ error))
+ return FALSE;
+ if (!fu_common_read_uint32_safe(buf_pkt->data,
+ buf_pkt->len,
+ COMMAND_OFFSET,
+ &cmd_tmp,
+ G_LITTLE_ENDIAN,
+ error))
+ return FALSE;
+ if (cmd_tmp != cmd) {
+ g_set_error(error,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "invalid sync message received, expected %x, got %x",
+ cmd,
+ cmd_tmp);
+ return FALSE;
+ }
+ if (!fu_common_read_uint32_safe(buf_pkt->data,
+ buf_pkt->len,
+ LENGTH_OFFSET,
+ &response_length,
+ G_LITTLE_ENDIAN,
+ error))
+ return FALSE;
+
+ fu_byte_array_append_uint32(buf_ack, CMD_ACK, G_LITTLE_ENDIAN); /* ACK message */
+ fu_byte_array_append_uint32(buf_ack, sizeof(guint32), G_LITTLE_ENDIAN); /* Length */
+ fu_byte_array_append_uint32(buf_ack, 0, G_LITTLE_ENDIAN); /* Sequence ID */
+ fu_byte_array_append_uint32(buf_ack, cmd, G_LITTLE_ENDIAN); /* Payload */
+ if (!fu_logitech_bulkcontroller_device_send(self, buf_ack, BULK_INTERFACE_SYNC, error))
+ return FALSE;
+ if (buf != NULL)
+ g_byte_array_append(buf, buf_pkt->data + SYNC_PACKET_HEADER_SIZE, response_length);
+
+ return TRUE;
+}
+
+static gchar *
+fu_logitech_bulkcontroller_device_compute_hash(GBytes *data)
+{
+ guint8 md5buf[HASH_VALUE_SIZE] = {0};
+ gsize data_len = sizeof(md5buf);
+ GChecksum *checksum = g_checksum_new(G_CHECKSUM_MD5);
+ g_checksum_update(checksum, g_bytes_get_data(data, NULL), g_bytes_get_size(data));
+ g_checksum_get_digest(checksum, (guint8 *)&md5buf, &data_len);
+ return g_base64_encode(md5buf, sizeof(md5buf));
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_write_firmware(FuDevice *device,
+ FuFirmware *firmware,
+ FwupdInstallFlags flags,
+ GError **error)
+{
+ FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device);
+ g_autofree gchar *base64hash = NULL;
+ g_autoptr(GByteArray) end_pkt = g_byte_array_new();
+ g_autoptr(GByteArray) start_pkt = g_byte_array_new();
+ g_autoptr(GBytes) fw = NULL;
+ g_autoptr(GPtrArray) chunks = NULL;
+
+ /* get default image */
+ fw = fu_firmware_get_bytes(firmware, error);
+ if (fw == NULL)
+ return FALSE;
+
+ /* Sending INIT */
+ if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self, CMD_INIT, NULL, error)) {
+ g_prefix_error(error, "error in writing init transfer packet: ");
+ return FALSE;
+ }
+
+ /* transfer sent */
+ fu_device_set_status(device, FWUPD_STATUS_DEVICE_WRITE);
+ fu_byte_array_append_uint64(start_pkt, g_bytes_get_size(fw), G_LITTLE_ENDIAN);
+ if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self,
+ CMD_START_TRANSFER,
+ start_pkt,
+ error)) {
+ g_prefix_error(error, "error in writing start transfer packet: ");
+ return FALSE;
+ }
+
+ /* each block */
+ chunks = fu_chunk_array_new_from_bytes(fw, 0x0, 0x0, PAYLOAD_SIZE);
+ for (guint i = 0; i < chunks->len; i++) {
+ FuChunk *chk = g_ptr_array_index(chunks, i);
+ g_autoptr(GByteArray) data_pkt = g_byte_array_new();
+ g_byte_array_append(data_pkt, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk));
+ if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self,
+ CMD_DATA_TRANSFER,
+ data_pkt,
+ error)) {
+ g_prefix_error(error, "failed to send data packet 0x%x: ", i);
+ return FALSE;
+ }
+ fu_device_set_progress_full(FU_DEVICE(self), i + 1, chunks->len);
+ }
+
+ /* sending end transfer */
+ base64hash = fu_logitech_bulkcontroller_device_compute_hash(fw);
+ fu_byte_array_append_uint32(end_pkt, 1, G_LITTLE_ENDIAN); /* update */
+ fu_byte_array_append_uint32(end_pkt, 0, G_LITTLE_ENDIAN); /* force */
+ fu_byte_array_append_uint32(end_pkt, MD5, G_LITTLE_ENDIAN); /* checksum type */
+ g_byte_array_append(end_pkt, (const guint8 *)base64hash, strlen(base64hash));
+ if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self,
+ CMD_END_TRANSFER,
+ end_pkt,
+ error)) {
+ g_prefix_error(error, "error in writing end transfer transfer packet: ");
+ return FALSE;
+ }
+
+ /* send uninit */
+ if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self, CMD_UNINIT, NULL, error)) {
+ g_prefix_error(error, "error in writing finish transfer packet: ");
+ return FALSE;
+ }
+
+ /* success! */
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_open(FuDevice *device, GError **error)
+{
+ FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device);
+ GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
+
+ /* FuUsbDevice->open */
+ if (!FU_DEVICE_CLASS(fu_logitech_bulkcontroller_device_parent_class)->open(device, error))
+ return FALSE;
+
+ /* claim both interfaces */
+ if (!g_usb_device_claim_interface(usb_device,
+ self->update_iface,
+ G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
+ error)) {
+ g_prefix_error(error, "failed to claim update interface: ");
+ return FALSE;
+ }
+ if (!g_usb_device_claim_interface(usb_device,
+ self->sync_iface,
+ G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
+ error)) {
+ g_prefix_error(error, "failed to claim sync interface: ");
+ return FALSE;
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_close(FuDevice *device, GError **error)
+{
+ FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device);
+ GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
+
+ if (!g_usb_device_release_interface(usb_device,
+ self->update_iface,
+ G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
+ error)) {
+ g_prefix_error(error, "failed to release update interface: ");
+ return FALSE;
+ }
+ if (!g_usb_device_release_interface(usb_device,
+ self->sync_iface,
+ G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
+ error)) {
+ g_prefix_error(error, "failed to release sync interface: ");
+ return FALSE;
+ }
+
+ /* FuUsbDevice->close */
+ return FU_DEVICE_CLASS(fu_logitech_bulkcontroller_device_parent_class)
+ ->close(device, error);
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_json_parser(FuDevice *device,
+ GByteArray *decoded_pkt,
+ GError **error)
+{
+ FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device);
+ JsonArray *json_devices;
+ JsonNode *json_root;
+ JsonObject *json_device;
+ JsonObject *json_object;
+ JsonObject *json_payload;
+ g_autoptr(JsonParser) json_parser = json_parser_new();
+
+ /* parse JSON reply */
+ if (!json_parser_load_from_data(json_parser,
+ (const gchar *)decoded_pkt->data,
+ decoded_pkt->len,
+ error)) {
+ g_prefix_error(error, "error in parsing json data: ");
+ return FALSE;
+ }
+ json_root = json_parser_get_root(json_parser);
+ if (json_root == NULL) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "did not get JSON root");
+ return FALSE;
+ }
+ json_object = json_node_get_object(json_root);
+ json_payload = json_object_get_object_member(json_object, "payload");
+ if (json_payload == NULL) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "did not get JSON payload");
+ return FALSE;
+ }
+ json_devices = json_object_get_array_member(json_payload, "devices");
+ if (json_devices == NULL) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "did not get JSON devices");
+ return FALSE;
+ }
+ json_device = json_array_get_object_element(json_devices, 0);
+ if (json_device == NULL) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "did not get JSON device");
+ return FALSE;
+ }
+ if (json_object_has_member(json_device, "name"))
+ fu_device_set_name(device, json_object_get_string_member(json_device, "name"));
+ if (json_object_has_member(json_device, "sw"))
+ fu_device_set_version(device, json_object_get_string_member(json_device, "sw"));
+ if (json_object_has_member(json_device, "type"))
+ fu_device_add_instance_id(device,
+ json_object_get_string_member(json_device, "type"));
+ if (json_object_has_member(json_device, "status"))
+ self->status = json_object_get_int_member(json_device, "status");
+ if (json_object_has_member(json_device, "updateStatus"))
+ self->update_status = json_object_get_int_member(json_device, "updateStatus");
+
+ return TRUE;
+}
+
+static gboolean
+fu_logitech_bulkcontroller_device_setup(FuDevice *device, GError **error)
+{
+ FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device);
+ g_autoptr(GByteArray) device_info_request = g_byte_array_new();
+ g_autoptr(GByteArray) decoded_pkt = g_byte_array_new();
+ g_autoptr(GByteArray) device_info_response = g_byte_array_new();
+ FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId;
+
+ /* FuUsbDevice->setup */
+ if (!FU_DEVICE_CLASS(fu_logitech_bulkcontroller_device_parent_class)->setup(device, error))
+ return FALSE;
+
+ /* sending GetDeviceInfoRequest */
+ device_info_request = proto_manager_generate_get_device_info_request();
+ if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self,
+ CMD_BUFFER_WRITE,
+ device_info_request,
+ error)) {
+ g_prefix_error(error, "error in sending buffer write packet: ");
+ return FALSE;
+ }
+
+ /* wait for the GetDeviceInfoResponse */
+ if (!fu_logitech_bulkcontroller_device_recv_sync_cmd(self,
+ CMD_BUFFER_READ,
+ device_info_response,
+ error)) {
+ g_prefix_error(error, "error in buffer read packet: ");
+ return FALSE;
+ }
+
+ if (!fu_logitech_bulkcontroller_device_recv_sync_cmd(self,
+ CMD_UNINIT_BUFFER,
+ NULL,
+ error)) {
+ g_prefix_error(error, "error in buffer read packet: ");
+ return FALSE;
+ }
+ if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self,
+ CMD_UNINIT_BUFFER,
+ NULL,
+ error)) {
+ g_prefix_error(error, "error in sending buffer uninitialize packet: ");
+ return FALSE;
+ }
+ decoded_pkt = proto_manager_decode_message(device_info_response->data,
+ device_info_response->len,
+ &proto_id,
+ error);
+ if (decoded_pkt == NULL) {
+ g_prefix_error(error, "error in unpacking packet: ");
+ return FALSE;
+ }
+ if (proto_id != kProtoId_GetDeviceInfoResponse) {
+ g_set_error_literal(error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "did not get kProtoId_GetDeviceInfoResponse");
+ return FALSE;
+ }
+ if (!fu_logitech_bulkcontroller_device_json_parser(device, decoded_pkt, error))
+ return FALSE;
+
+ /* success */
+ return TRUE;
+}
+
+static void
+fu_logitech_bulkcontroller_device_init(FuLogitechBulkcontrollerDevice *self)
+{
+ fu_device_add_protocol(FU_DEVICE(self), "com.logitech.vc.proto");
+ fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET);
+ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
+}
+
+static void
+fu_logitech_bulkcontroller_device_class_init(FuLogitechBulkcontrollerDeviceClass *klass)
+{
+ FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
+ klass_device->to_string = fu_logitech_bulkcontroller_device_to_string;
+ klass_device->write_firmware = fu_logitech_bulkcontroller_device_write_firmware;
+ klass_device->probe = fu_logitech_bulkcontroller_device_probe;
+ klass_device->setup = fu_logitech_bulkcontroller_device_setup;
+ klass_device->open = fu_logitech_bulkcontroller_device_open;
+ klass_device->close = fu_logitech_bulkcontroller_device_close;
+}
diff --git a/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.h b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.h
new file mode 100644
index 000000000..2bbce5e34
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2021 Richard Hughes
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include
+
+#define FU_TYPE_LOGITECH_BULKCONTROLLER_DEVICE (fu_logitech_bulkcontroller_device_get_type())
+G_DECLARE_FINAL_TYPE(FuLogitechBulkcontrollerDevice,
+ fu_logitech_bulkcontroller_device,
+ FU,
+ LOGITECH_BULKCONTROLLER_DEVICE,
+ FuUsbDevice)
diff --git a/plugins/logitech-bulkcontroller/fu-plugin-logitech-bulkcontroller.c b/plugins/logitech-bulkcontroller/fu-plugin-logitech-bulkcontroller.c
new file mode 100644
index 000000000..01526d135
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/fu-plugin-logitech-bulkcontroller.c
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include
+
+#include "fu-logitech-bulkcontroller-device.h"
+
+void
+fu_plugin_init(FuPlugin *plugin)
+{
+ fu_plugin_set_build_hash(plugin, FU_BUILD_HASH);
+ fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_BULKCONTROLLER_DEVICE);
+}
diff --git a/plugins/logitech-bulkcontroller/logitech-bulkcontroller.quirk b/plugins/logitech-bulkcontroller/logitech-bulkcontroller.quirk
new file mode 100644
index 000000000..4eae110ce
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/logitech-bulkcontroller.quirk
@@ -0,0 +1,10 @@
+# TODO: revisit InstallDuration
+
+[USB\VID_046D&PID_089B]
+Plugin = logitech_bulkcontroller
+InstallDuration = 1500
+
+[USB\VID_046D&PID_08D3]
+Plugin = logitech_bulkcontroller
+InstallDuration = 1500
+Flags = is-mini
diff --git a/plugins/logitech-bulkcontroller/meson.build b/plugins/logitech-bulkcontroller/meson.build
new file mode 100644
index 000000000..a7e5c6bc1
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/meson.build
@@ -0,0 +1,37 @@
+if get_option('plugin_logitech_bulkcontroller')
+
+if not get_option('gusb')
+ error('gusb is required for plugin_logitech_bulkcontroller')
+endif
+
+cargs = ['-DG_LOG_DOMAIN="FuPluginLogitechBulkController"']
+
+install_data(['logitech-bulkcontroller.quirk'],
+ install_dir: join_paths(get_option('datadir'), 'fwupd', 'quirks.d')
+)
+subdir('proto')
+shared_module('fu_plugin_logitech_bulkcontroller',
+ fu_hash,
+ sources : [
+ generated,
+ 'fu-logitech-bulkcontroller-common.c',
+ 'fu-logitech-bulkcontroller-device.c',
+ 'fu-plugin-logitech-bulkcontroller.c',
+ ],
+ include_directories : [
+ root_incdir,
+ fwupd_incdir,
+ fwupdplugin_incdir,
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ link_with : [
+ fwupd,
+ fwupdplugin,
+ ],
+ c_args : cargs,
+ dependencies : [
+ plugin_deps,
+ ],
+)
+endif
diff --git a/plugins/logitech-bulkcontroller/proto/antiflicker.proto b/plugins/logitech-bulkcontroller/proto/antiflicker.proto
new file mode 100644
index 000000000..5dbc2bc29
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/antiflicker.proto
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+import "device_common.proto";
+
+/**
+ * This message data structure holds information about the
+ * current AntiFlicker configuration.
+ *
+ */
+message AntiFlickerConfiguration
+{
+ enum Mode {
+ NTSC_60HZ = 0;
+ PAL_50HZ = 1;
+ }
+
+ Mode mode = 1;
+}
+
+message SetAntiFlickerConfigurationRequest
+{
+ AntiFlickerConfiguration.Mode mode = 1;
+}
+
+message SetAntiFlickerConfigurationResponse
+{
+ bool success = 1;
+
+ repeated Error errors = 2;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/ble_cfg.proto b/plugins/logitech-bulkcontroller/proto/ble_cfg.proto
new file mode 100644
index 000000000..f1306847d
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/ble_cfg.proto
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+import "device_common.proto";
+
+message SetBLECfgRequest
+{
+ /**
+ * (REQUIRED) If true, BLE is enabled and active otherwise disabled
+ */
+ bool BLE_ON = 1;
+}
+
+message SetBLECfgResponse
+{
+ bool success = 1;
+ repeated Error errors = 2;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/crash_info.proto b/plugins/logitech-bulkcontroller/proto/crash_info.proto
new file mode 100644
index 000000000..159846853
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/crash_info.proto
@@ -0,0 +1,285 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Kong as an Android device can accumulate
+ * crash debug information during its operation.
+ * When Kong is running in device mode, those
+ * crash dump files need to be copied over to
+ * PC and uploaded to S3.
+ * Note, if Kong is running in host mode, uploaded
+ * files, and then moved to device mode, will it
+ * copy the same files over?
+ *
+ * This message requests that crash dump files be
+ * copied over to PC
+ *
+ * EXPECTED RESPONSE
+ * SendCrashDumpResponse
+ *
+ */
+message SendCrashDumpRequest
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+}
+
+/**
+ * Crash dump information. Most of these
+ * are supplied by the crash analytics service, so lets
+ * pass this information along.
+ */
+message CrashDumpInfo
+{
+ /**
+ * the filename
+ */
+ string file_name = 1;
+
+ /**
+ * the serial number
+ */
+ string device_id = 2;
+
+ /**
+ * the software version
+ */
+ string software_version = 3;
+
+ /**
+ * the file size
+ */
+ uint64 file_size = 4;
+
+ /**
+ * timestamp
+ */
+ uint64 timestamp = 5;
+
+ /**
+ * md5 for file
+ */
+ string md5 = 6;
+
+ /**
+ * the device type . Kong|Diddy
+ */
+ string device_type = 7;
+
+ /**
+ * the device mode. Hosted|Appliance
+ */
+ string device_mode = 8;
+
+ /**
+ * the report type. BugReport|EventLog,Diagnostics
+ */
+ string report_type = 9;
+
+ /**
+ * the content type. application/zip | text/plain | application/json
+ */
+ string content_type = 10;
+}
+
+/**
+ * Response which contains the crash dump file name
+ * information and bool value to indicate will send
+ * file
+ */
+message SendCrashDumpResponse
+{
+ /**
+ * (OPTIONAL)
+ * If crash dump exists, this variable
+ * contains the file name of crash dump
+ * that will be copied over.
+ */
+ string crash_dump_file = 1;
+
+ /**
+ * (REQUIRED)
+ * bool value to indicate will send file
+ * true if sending file over.
+ * false if no file to send.
+ * If true, caller will look at CrashDumpInfo
+ */
+ bool will_send_file = 2;
+
+ /**
+ * (OPTIONAL)
+ * Crash dump info
+ */
+ CrashDumpInfo crash_dump_info = 3;
+}
+
+message SendCrashDumpRequestv2
+{
+ /**
+ * The attestation challenge.
+ * (REQUIRED)
+ */
+ string challenge = 1;
+
+ /**
+ * Time to live
+ * (REQUIRED)
+ */
+ int32 ttl = 2;
+}
+
+/**
+ * Response which contains the crash dump file name
+ * information, bool value to indicate will send
+ * file, body of the request and signature
+ */
+message SendCrashDumpResponsev2
+{
+ /**
+ * (OPTIONAL)
+ * If crash dump exists, this variable
+ * contains the file name of crash dump
+ * that will be copied over.
+ */
+ string crash_dump_file = 1;
+
+ /**
+ * (REQUIRED)
+ * bool value to indicate will send file
+ * true if sending file over.
+ * false if no file to send.
+ * If true, caller will look at CrashDumpInfo
+ */
+ bool will_send_file = 2;
+
+ /**
+ * (OPTIONAL)
+ * The get upload url body. This is a json string
+ */
+ string body = 3;
+
+ /**
+ * (OPTIONAL)
+ * The get upload url body signature.
+ */
+ string signature = 4;
+}
+
+/**
+ * This is event sent from PC or Kong to indicate
+ * Success
+ */
+message SendCrashDumpEvent
+{
+ /**
+ * (REQUIRED)
+ * Contains the file name of crash dump
+ * that is being sent or in process of being
+ * received
+ */
+ string crash_dump_file = 1;
+
+ /**
+ * (REQUIRED)
+ * Transfer state.
+ * true indicates file was received without errors and bug report file was
+ * uploaded false means an error occurred
+ */
+ bool success = 2;
+}
+
+/**
+ * Place holder for Android requesting that a crash dump copy
+ * get initiated from PC side
+ */
+message CrashDumpAvailableEvent
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+}
+
+/**
+ * Ask device to generate a bug report. This could be
+ * for gathering logcat, system logs, etc.
+ * Similar to SendCrashDumpRequestv2, but bug report generation is on
+ * demand.
+ * EXPECTED RESPONSE:
+ * GenerateCrashDumpResponse
+ * It should follow the same flow as described here
+ * https://docs.google.com/document/d/1D5nx1nenDu9ucZbYPXlNNxFEN1tx3W7k044mvi74x28/edit#heading=h.a9wyfbpb2282
+ */
+message GenerateCrashDumpRequest
+{
+ /**
+ * The attestation challenge.
+ * (REQUIRED)
+ */
+ string challenge = 1;
+
+ /**
+ * Time to live
+ * (REQUIRED)
+ */
+ int32 ttl = 2;
+
+ /**
+ * The note to include in the bug report. This could be empty.
+ * (OPTIONAL)
+ */
+ string note = 3;
+}
+
+/**
+ * Response which contains the
+ * crash dump file name information,
+ * bool value to indicate will send file,
+ * body of the request and signature.
+ * Similar to SendCrashDumpResponsev2, but bug report generation is on
+ * demand.
+ * It should follow the same flow as described here
+ * https://docs.google.com/document/d/1D5nx1nenDu9ucZbYPXlNNxFEN1tx3W7k044mvi74x28/edit#heading=h.a9wyfbpb2282
+ */
+message GenerateCrashDumpResponse
+{
+ /**
+ * (OPTIONAL)
+ * If crash dump exists, this variable
+ * contains the file name of crash dump
+ * that will be copied over.
+ */
+ string crash_dump_file = 1;
+
+ /**
+ * (REQUIRED)
+ * bool value to indicate will send file
+ * true if sending file over.
+ * false if no file to send.
+ */
+ bool will_send_file = 2;
+
+ /**
+ * (OPTIONAL)
+ * The get upload url body. This is a json string
+ */
+ string body = 3;
+
+ /**
+ * (OPTIONAL)
+ * The get upload url body signature.
+ */
+ string signature = 4;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/device_attestation.proto b/plugins/logitech-bulkcontroller/proto/device_attestation.proto
new file mode 100644
index 000000000..ac3554d80
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/device_attestation.proto
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Request for certificate chain
+ * This is to be included in UsbMsg
+
+ * EXPECTED RESPONSE
+ * GetCertificateChainResponse
+ */
+message GetCertificateChainRequest
+{
+ /**
+ * attestation challenge
+ */
+ string attestation = 1;
+
+ /**
+ * time to live
+ */
+ int32 ttl = 2;
+}
+
+/**
+ * Get certificate chain response
+ */
+message GetCertificateChainResponse
+{
+ /**
+ * array of certs
+ */
+ repeated string certchain = 1;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/device_common.proto b/plugins/logitech-bulkcontroller/proto/device_common.proto
new file mode 100644
index 000000000..4d46dd0bb
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/device_common.proto
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * This error messages describe a failure that was encountered
+ * by the Sync service and primarily consist of an error code
+ * and a short, human-readable message. Therefore, if a client
+ * receives a message with a field reserved for Error messages,
+ * it is prudent that the application first check if there are
+ * errors before doing any further processing of the message.
+ */
+message Error
+{
+ /**
+ * (REQUIRED) Error code.
+ */
+ uint32 error_code = 1;
+
+ /**
+ * (OPTIONAL) Short, human-readable error message. If no
+ * message is available, then this will be an empty string.
+ */
+ string error_message = 2;
+
+ /**
+ * (OPTIONAL) A URI to a log file or some other document
+ * that contains more detailed information about the error.
+ * If such a file is not available, this will be an empty
+ * string.
+ */
+ string error_log_uri = 3;
+
+ /**
+ * (OPTIONAL) An optional JSON string with additional
+ * metadata that may be useful to the client.
+ */
+ string json_metadata = 4;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/device_info.proto b/plugins/logitech-bulkcontroller/proto/device_info.proto
new file mode 100644
index 000000000..2efe51826
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/device_info.proto
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Request Device information
+ * This is to be included in UsbMsg
+
+ * EXPECTED RESPONSE
+ * GetDeviceInfoResponse
+ */
+message GetDeviceInfoRequest
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+}
+
+/**
+ * Get device information response
+ */
+message GetDeviceInfoResponse
+{
+ /**
+ * payload contains actual mqtt message
+ */
+ string payload = 1;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/device_mode.proto b/plugins/logitech-bulkcontroller/proto/device_mode.proto
new file mode 100644
index 000000000..426e85a7b
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/device_mode.proto
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Behavior change as of 1/28/2021 EE
+ * Kong sync-agent should not deprovision when this message is
+ * received. If would just start forwarding events to PC when message is
+ * received.
+ *
+ * (Legacy)
+ * Request to transition to device mode
+ * Kong could be provisioned in Host mode. This message
+ * will ask Kong to deprovisioned/remove host mode provisioning
+ * data.
+ * This is to be included in UsbMsg
+
+ * EXPECTED RESPONSE
+ * TransitionToDeviceModeResponse
+ */
+message TransitionToDeviceModeRequest
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+}
+
+/**
+ * Request to transition to device mode response
+ */
+message TransitionToDeviceModeResponse
+{
+ /**
+ * boolean value to indicate Kong was able to transition to
+ * device mode. If Kong is not provisioned, should just respond
+ * with true value.
+ * set to false if error was encountered during transition, and Kong
+ * wasn't able to transition (is this possible?)
+ */
+ bool success = 1;
+
+ /**
+ * the error in integer if success was false
+ */
+ int32 error = 2;
+
+ /**
+ * the error description
+ */
+ string error_description = 3;
+}
+
+/**
+ * Added 1/28/2021 EE
+ * Request to deprovision Kong
+ * This request is sent by PC sync-agent when PC
+ * is provisioned.
+ * Kong sync-agent should deprovision (if provisioned)
+ *
+ * EXPECTED RESPONSE
+ * SetDeprovisionResponse
+ */
+message SetDeprovisionRequest
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+}
+
+/**
+ * Response to deprovision request
+ */
+message SetDeprovisionResponse
+{
+ /**
+ * boolean value to indicate Kong was able to deprovision Kong.
+ * If Kong is not provisioned, should just respond
+ * with true value.
+ * set to false if error was encountered during deprovisioning.
+ */
+ bool success = 1;
+
+ /**
+ * the error in integer if success was false
+ */
+ int32 error = 2;
+
+ /**
+ * the error description
+ */
+ string error_description = 3;
+}
+
+/**
+ * Added 3/22/2021 EE
+ * For sending a certificate as data. There are currently
+ * 2 known certificate that will be transferred - Root CA, and 802.1x cert.
+ * Upon receipt, sync-agent should verify using the supplied hash
+ * and write the data to the file system.
+ *
+ * EXPECTED RESPONSE
+ * SendCertificateDataResponse
+ */
+message SendCertificateDataRequest
+{
+ /**
+ * The certificate type
+ */
+ enum CertType {
+ /**
+ * Reserved. Do not use.
+ */
+ RESERVED = 0;
+ /**
+ * Root CA
+ */
+ ROOT_CA = 1;
+ /**
+ * 802.1x cert
+ */
+ NET_CONFIG = 2;
+ }
+
+ /**
+ * (REQUIRED)
+ * The certificate type
+ */
+ CertType cert_type = 1;
+
+ /**
+ * (REQUIRED)
+ * the certificate file name
+ */
+ string file_name = 2;
+
+ /**
+ * (REQUIRED)
+ * the certificate data
+ */
+ bytes cert_data = 3;
+
+ /**
+ * (REQUIRED)
+ * the certificate md5 hash
+ */
+ string md5 = 4;
+}
+
+/**
+ * Response to SendCertificateData Request
+ */
+message SendCertificateDataResponse
+{
+ /**
+ * (REQUIRED)
+ * boolean value to indicate data was received, hash verified .
+ * set to false if error was encountered during transfer and verification.
+ */
+ bool success = 1;
+
+ /**
+ * (OPTIONAL)
+ * the error in integer if success was false
+ */
+ int32 error = 2;
+
+ /**
+ * (OPTIONAL)
+ * the error description if there are errors
+ */
+ string error_description = 3;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/device_request.proto b/plugins/logitech-bulkcontroller/proto/device_request.proto
new file mode 100644
index 000000000..3969c73d3
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/device_request.proto
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Request to reboot device
+ * This is to be included in UsbMsg
+
+ * EXPECTED RESPONSE
+ * RebootDeviceResponse
+ */
+message RebootDeviceRequest
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+
+ /**
+ * A timestamp indicating when the reboot request
+ * was initiated.
+ * The device should include this entry as part of the event information
+ * it sends back to PC during a reboot request.
+ */
+ uint64 iat = 2;
+}
+
+/**
+ * Reboot device response
+ */
+message RebootDeviceResponse
+{
+ /**
+ * bool value to indicate reboot was requested. If there are errors
+ * while requesting a device to reboot, should set the value to false
+ */
+ bool success = 1;
+}
+
+/**
+ * This message requests that the speaker boost audio setting be changed.
+ * The device should send a device info event after this setting request are
+ * handled.
+ *
+ * EXPECTED RESPONSE
+ * SetSpeakerBoostResponse
+ *
+ */
+message SetSpeakerBoostRequest
+{
+ /**
+ * (REQUIRED) The speaker boost setting to be set
+ *
+ * If value is 0, the request is to disable. If 1,
+ * the request is to enable.
+ */
+ int32 speaker_boost = 1;
+}
+
+message SetSpeakerBoostResponse
+{
+ /**
+ * (REQUIRED) set to true if the audio setting request was successfully sent,
+ * false otherwise
+ */
+ bool success = 1;
+}
+
+/**
+ * This message requests that the noise reduction audio setting be changed.
+ * The device should send a device info event after this setting request are
+ * handled.
+ *
+ * EXPECTED RESPONSE
+ * SetNoiseReductionResponse
+ *
+ */
+message SetNoiseReductionRequest
+{
+ /**
+ * (REQUIRED) The noise reduction setting to be set
+ *
+ * If value is 0, the request is to disable. If 1,
+ * the request is to enable.
+ */
+ int32 noise_reduction = 1;
+}
+
+message SetNoiseReductionResponse
+{
+ /**
+ * (REQUIRED) set to true if the audio setting request was successfully sent,
+ * false otherwise
+ */
+ bool success = 1;
+}
+
+/**
+ * This message requests that the reverb mode audio setting be changed.
+ * The device should send a device info event after this setting request are
+ * handled.
+ *
+ * EXPECTED RESPONSE
+ * SetReverbModeResponse
+ *
+ */
+message SetReverbModeRequest
+{
+ /**
+ * Reverb mode enumeration
+ */
+ enum ReverbMode {
+ DISABLED = 0;
+ MILD = 1;
+ NORMAL = 2;
+ AGGRESSIVE = 3;
+ }
+
+ /**
+ * (REQUIRED) The reverb mode setting to be set
+ *
+ * see Reverb mode enumeration
+ */
+ ReverbMode reverb_mode = 1;
+}
+
+message SetReverbModeResponse
+{
+ /**
+ * (REQUIRED) set to true if the setting request was successfully sent, false
+ * otherwise
+ */
+ bool success = 1;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/device_time.proto b/plugins/logitech-bulkcontroller/proto/device_time.proto
new file mode 100644
index 000000000..7db54028f
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/device_time.proto
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Request for setting device time
+ * This is to be included in UsbMsg
+ */
+message SetDeviceTimeRequest
+{
+ /**
+ * utc timestamp.
+ */
+ uint64 ts = 1;
+ /**
+ * the time zone.
+ */
+ string time_zone = 2;
+}
+
+/**
+ * Send an ack as the response
+ */
diff --git a/plugins/logitech-bulkcontroller/proto/firmware_update.proto b/plugins/logitech-bulkcontroller/proto/firmware_update.proto
new file mode 100644
index 000000000..67e904249
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/firmware_update.proto
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Request to start update
+ * This is to be included in UsbMsg
+
+ * EXPECTED RESPONSE
+ * UpdateNowResponse
+ */
+message UpdateNowRequest
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+}
+
+/**
+ * Update now response
+ */
+message UpdateNowResponse
+{
+ /**
+ * bool value to indicate update was started
+ */
+ bool started = 1;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/meson.build b/plugins/logitech-bulkcontroller/proto/meson.build
new file mode 100644
index 000000000..625737d9b
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/meson.build
@@ -0,0 +1,21 @@
+
+gen = generator(protoc, \
+ output : ['@BASENAME@.pb-c.c', '@BASENAME@.pb-c.h'],
+ arguments : ['--proto_path=@CURRENT_SOURCE_DIR@', '--c_out=@BUILD_DIR@', '@INPUT@'])
+
+src = [
+ 'antiflicker.proto',
+ 'ble_cfg.proto',
+ 'crash_info.proto',
+ 'device_attestation.proto',
+ 'device_common.proto',
+ 'device_info.proto',
+ 'device_mode.proto',
+ 'device_request.proto',
+ 'device_time.proto',
+ 'firmware_update.proto',
+ 'rightsight.proto',
+ 'ota_manifest.proto',
+ 'usb_msg.proto',
+ ]
+generated = gen.process(src)
diff --git a/plugins/logitech-bulkcontroller/proto/ota_manifest.proto b/plugins/logitech-bulkcontroller/proto/ota_manifest.proto
new file mode 100644
index 000000000..3efed3a85
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/ota_manifest.proto
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+/**
+ * Request device to create a GetManifestv2 body. See
+ * https://docs.google.com/document/d/1l31A1TWhtJC0xR8GwuNtiGN4vPLURRsj5ZcC1uEIwVQ/edit#heading=h.ctbthi1iyxw1
+ *
+ *
+ * This is to be included in UsbMsg
+ *
+ * EXPECTED RESPONSE
+ * GetManifestBodyResponse
+ */
+message GetManifestBodyRequest
+{
+ /**
+ * The attestation challenge.
+ * (REQUIRED)
+ */
+ string challenge = 1;
+
+ /**
+ * The manifest version.
+ * (REQUIRED)
+ */
+ string version = 2;
+
+ /**
+ * The channel. Dont use if empty or null
+ * (OPTIONAL)
+ */
+ string channel = 3;
+
+ /**
+ * The meta info in json format. This
+ * field usually comes from PC.
+ * (OPTIONAL)
+ */
+ string meta_info = 4;
+
+ /**
+ * Time to live
+ * (REQUIRED)
+ */
+ int32 ttl = 5;
+}
+
+/**
+ * GetManifestv2 body response
+ */
+message GetManifestBodyResponse
+{
+ /**
+ * The get manifest body. This is a json string
+ */
+ string body = 1;
+
+ /**
+ * The get manifest body signature.
+ */
+ string signature = 2;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/rightsight.proto b/plugins/logitech-bulkcontroller/proto/rightsight.proto
new file mode 100644
index 000000000..771ed4e1e
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/rightsight.proto
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+import "device_common.proto";
+
+/**
+ * This message data structure holds information about the
+ * current RightSight configuration.
+ *
+ */
+message RightSightConfiguration
+{
+ /**
+ * Enumeration of modes that the RightSight service can be in.
+ */
+ enum Mode {
+ /**
+ * This does not indicate a default value.
+ *
+ */
+ DO_NOT_USE = 0;
+
+ /**
+ * The camera will continually pan, tilt, and zoom
+ * to properly frame everyone during a meeting.
+ */
+ DYNAMIC = 1;
+
+ /**
+ * The camera will pan, tilt, and zoom to properly in
+ * the meeting only when the call starts.
+ */
+ ON_CALL_START = 2;
+ }
+
+ /**
+ * (REQUIRED) If true, RightSight is enabled and active.
+ */
+ bool enabled = 1;
+
+ /**
+ * (REQUIRED) The current mode that RightSight is in.
+ */
+ Mode mode = 2;
+
+ /**
+ * (REQUIRED) A timestamp indicating when the RightSight
+ * settings were last modified. This is the number of
+ * milliseconds since the epoch.
+ */
+ uint64 last_modified = 3;
+}
+
+/**
+ * RightSight is an auto-framing feature that is available in Kong.
+ * With RightSight enabled, your device will automatically pan, tilt, and zoom
+ * the camera lens in order to capture all meeting participants
+ * within the image frame. This feature can be set to one of two
+ * modes: dynamic and on call start. When in dynamic mode, the
+ * device will actively pan, tilt, and zoom the camera lens when
+ * appropriate in order to keep all participants in frame during
+ * the entire course of the meeting. When in on call start mode,
+ * the camera lens will pan, tilt, and zoom to capture everybody
+ * in frame only when the meeting starts.
+ *
+ * When RightSight is enabled, it is set
+ * to dynamic mode by default.
+ *
+ * This message requests that the RightSight configuration
+ * settings be changed.
+ *
+ * EXPECTED RESPONSE
+ * SetRightSightConfigurationResponse
+ *
+ */
+message SetRightSightConfigurationRequest
+{
+ /**
+ * (REQUIRED) If true, requests that RightSight be
+ * turned on. If false, indicates that
+ * RightSight should be turned off.
+ */
+ bool enabled = 1;
+
+ /**
+ * (REQUIRED) The mode for RightSight to be in. A value is
+ * required, but if none is provided, then this will
+ * default to DYNAMIC mode.
+ *
+ * If enabled is set to false, then this will effectively
+ * do nothing as RightSight is turned off.
+ */
+ RightSightConfiguration.Mode mode = 2;
+}
+
+/**
+ * Response which contains the RightSight configuration that was
+ * set as a result of the request.
+ */
+message SetRightSightConfigurationResponse
+{
+ /**
+ * (OPTIONAL) If any errors occurred while processing the
+ * request, then this field should be set accordingly.
+ */
+ repeated Error errors = 1;
+
+ /**
+ * (REQUIRED) The RightSight configuration that was set on
+ * the product.
+ */
+ RightSightConfiguration right_sight_configuration = 2;
+}
diff --git a/plugins/logitech-bulkcontroller/proto/usb_msg.proto b/plugins/logitech-bulkcontroller/proto/usb_msg.proto
new file mode 100644
index 000000000..95f40cfa2
--- /dev/null
+++ b/plugins/logitech-bulkcontroller/proto/usb_msg.proto
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 1999-2021 Logitech, Inc.
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+syntax = "proto3";
+
+package logi.device.proto;
+
+option java_package = "com.logitech.vc.proto";
+
+import "device_info.proto";
+import "firmware_update.proto";
+import "crash_info.proto";
+import "device_mode.proto";
+import "device_attestation.proto";
+import "rightsight.proto";
+import "ota_manifest.proto";
+import "device_time.proto";
+import "ble_cfg.proto";
+import "antiflicker.proto";
+import "device_request.proto";
+
+/**
+ *
+ * Header message to be included in UsbMsg. This contains
+ * message metadata that aids in processing of messages
+ */
+message Header
+{
+ /**
+ * A unique id of the message. If responding after receiving
+ * data, the value stored in this field should be used in the ack message
+ * msgId field
+ */
+ string id = 1;
+ /**
+ * A timestamp indicating when the message was
+ * sent. This is the number of milliseconds that have
+ * elapsed since the epoch, in string format
+ */
+ string timestamp = 2;
+}
+
+/**
+ * The Ack message.
+ * This is to be included in UsbMsg
+ */
+message Acknowledge
+{
+ /**
+ * The message Id. This should be the same value
+ * in UsbMsg.Header.id field
+ */
+ string msgId = 1;
+
+ /**
+ * The message processing result. true indicates message was
+ * successfully processed, false otherwise.
+ */
+ bool success = 2;
+}
+
+/**
+ * The Kong Event message.
+ * Anything that is not part of
+ * Request/Response messaging, but is being sent to mqtt distributor
+ * should be considered as a KongEvent, and forwarded to device host.
+ * This is to be included in UsbMsg
+ */
+message KongEvent
+{
+ /**
+ * mqtt_event contains actual mqtt message
+ */
+ string mqtt_event = 1;
+}
+
+/**
+ * Sent by Kong sync-agent.
+ * If Kong sync-agent starts-up and it is in Device mode, then
+ * it can send this event. When PC sync-agent receives this event,
+ * it should send a TransitionToDeviceModeRequest.
+ * This is to be included in UsbMsg
+ */
+message HandshakeEvent
+{
+ /**
+ * Unused. Reserved for future use.
+ */
+ bool reserved = 1;
+}
+
+/**
+ * The enclosing message.
+ * This is the root message of all messagesszx
+ */
+message UsbMsg
+{
+ /**
+ * Header for the message containing additional
+ * message metadata.
+ */
+ Header header = 1;
+
+ /**
+ * The actual message being sent. One of these must be
+ * included
+ */
+ oneof message
+ {
+ /**
+ * Ack message
+ */
+ Acknowledge ack = 2;
+ /**
+ * Request message
+ */
+ Request request = 3;
+ /**
+ * Response message
+ */
+ Response response = 4;
+ /**
+ * Event
+ */
+ Event event = 5;
+ }
+}
+
+/**
+ * The Request message.
+ * This is to be included in UsbMsg
+ */
+message Request
+{
+ oneof payload
+ {
+ GetDeviceInfoRequest get_device_info_request = 2;
+ UpdateNowRequest update_now_request = 3;
+ SendCrashDumpRequest crash_dump_request = 4;
+ TransitionToDeviceModeRequest transition_to_devicemode_request = 5;
+ GetCertificateChainRequest get_certificate_chain_request = 6;
+ SetRightSightConfigurationRequest set_right_sight_configuration_request = 7;
+ GetManifestBodyRequest get_manifest_body_request = 8;
+ SendCrashDumpRequestv2 crash_dump_request_v2 = 9;
+ SetDeviceTimeRequest set_device_time_request = 10;
+ SetAntiFlickerConfigurationRequest set_anti_flicker_configuration_request = 11;
+ SetBLECfgRequest set_ble_cfg_request = 12;
+ SetDeprovisionRequest set_deprovision_request = 13;
+ RebootDeviceRequest reboot_device_request = 14;
+ SetSpeakerBoostRequest speaker_boost_request = 15;
+ SetNoiseReductionRequest noise_reduction_request = 16;
+ SetReverbModeRequest reverb_mode_request = 17;
+ GenerateCrashDumpRequest generate_bug_report_request = 18;
+ SendCertificateDataRequest send_certificate_data_request = 19;
+ }
+}
+
+/**
+ * The Response message.
+ * This is to be included in UsbMsg
+ */
+message Response
+{
+ oneof payload
+ {
+ GetDeviceInfoResponse get_device_info_response = 2;
+ UpdateNowResponse update_now_response = 3;
+ SendCrashDumpResponse crash_dump_response = 4;
+ TransitionToDeviceModeResponse transition_to_devicemode_response = 5;
+ GetCertificateChainResponse get_certificate_chain_response = 6;
+ SetRightSightConfigurationResponse set_right_sight_configuration_response = 7;
+ GetManifestBodyResponse get_manifest_body_response = 8;
+ SendCrashDumpResponsev2 crash_dump_response_v2 = 9;
+ SetAntiFlickerConfigurationResponse set_anti_flicker_configuration_response = 11;
+ SetBLECfgResponse set_ble_cfg_response = 12;
+ SetDeprovisionResponse set_deprovision_response = 13;
+ RebootDeviceResponse reboot_device_response = 14;
+
+ SetSpeakerBoostResponse speaker_boost_response = 15;
+ SetNoiseReductionResponse noise_reduction_response = 16;
+ SetReverbModeResponse reverb_mode_response = 17;
+ GenerateCrashDumpResponse generate_bug_report_response = 18;
+ SendCertificateDataResponse send_certificate_data_response = 19;
+ }
+}
+
+/**
+ * The Event message.
+ * This is to be included in UsbMsg
+ */
+message Event
+{
+ oneof payload
+ {
+ KongEvent kong_event = 1;
+ SendCrashDumpEvent send_crash_dump_event = 2;
+ CrashDumpAvailableEvent crash_dump_available_event = 3;
+ HandshakeEvent handshake_event = 4;
+ }
+}
diff --git a/plugins/meson.build b/plugins/meson.build
index f97ed2524..4df17e769 100644
--- a/plugins/meson.build
+++ b/plugins/meson.build
@@ -35,6 +35,7 @@ subdir('linux-swap')
subdir('linux-tainted')
subdir('logind')
subdir('logitech-hidpp')
+subdir('logitech-bulkcontroller')
subdir('modem-manager')
subdir('msr')
subdir('nitrokey')
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 29ccee5f5..f27a1e0c9 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -211,11 +211,13 @@ parts:
- libpango1.0-dev
- libpci-dev
- libpolkit-gobject-1-dev
+ - libprotobuf-c-dev
- libsmbios-dev
- libsqlite3-dev
- libsystemd-dev
- locales
- pkg-config
+ - protobuf-c-compiler
- systemd
- uuid-dev
stage-packages: