diff --git a/.github/instructions/cpp-style.instructions.md b/.github/instructions/cpp-style.instructions.md index a81c7f6..cba3ea5 100644 --- a/.github/instructions/cpp-style.instructions.md +++ b/.github/instructions/cpp-style.instructions.md @@ -21,6 +21,7 @@ Every `.c`/`.h`/`.cpp`/`.hpp` starts with this block (ISO 8601 dates, no * @version 1.0.0 * @authors * @date YYYY-MM-DD + * @date @showdate "%Y-%m-%d" */ ``` @@ -147,6 +148,24 @@ success, leave it at the error default otherwise. | Enum values | `eCamelCase` | `eSoftware`, `eWatchDog` | | Module-level statics | `lower_snake_case`| `system_clock_hz` | +Pointer and reference declarators stay next to the variable name, not the type: + +```cpp +int *value_ptr; +const char *name; +std::string &value_ref; +const std::vector &values; +``` +The exception is the return types of functions: +```cpp +static libusb_device* findDeviceByInfo( + libusb_device **deviceList, + const SUsbDeviceInfo &deviceInfo, + const ssize_t deviceCount +); +``` + +The structure variables are placed taking into account alignment and the minimum amount of padding. ## Conditional compilation diff --git a/.gitignore b/.gitignore index 813a39d..83009db 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,12 @@ install_manifest.txt compile_commands.json CTestTestfile.cmake _deps +CMakePresets.json CMakeUserPresets.json +# VS code user files +.vscode/* + # Python cache __pycache__/ diff --git a/HISTORY.md b/HISTORY.md index 2e63fc9..624903c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -79,10 +79,17 @@ Records key decisions, structural changes, and completed development stages. - Rescan automatically when switching from Demo mode to Live mode. - Verified detection with a connected Hantek DSO-2250 and a successful Release build. +- Implemented device opening, USB interface claiming, interface release, and + handle/context cleanup for the selected Hantek DSO-2250. +- Added Connect and Disconnect controls with connection state in the status + line. +- Disconnect automatically when entering Demo mode, on device loss after a + rescan, and during application shutdown. +- Added a Linux udev rule for non-root DSO-2250 access through libusb. +- Verified the connect/disconnect lifecycle with a connected Hantek DSO-2250. ### Next USB tasks -- Connect to the selected device and claim its USB interface. - Read endpoint data chunks in a dedicated acquisition path. - Add buffering between USB reads and waveform processing. - Handle timeouts, I/O errors, disconnects, and recovery states. diff --git a/README.md b/README.md index dc985a3..e1a05b5 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,28 @@ The current application requires: - GNU Make for builds through `make`. - SDL2 development files; - OpenGL development files. +- libusb-1.0 development files. On Debian, Ubuntu, and Linux Mint: ```bash sudo apt-get update -sudo apt-get install build-essential cmake make pkg-config libsdl2-dev libgl1-mesa-dev +sudo apt-get install build-essential cmake make pkg-config libsdl2-dev libgl1-mesa-dev libusb-1.0-0-dev ``` -Dear ImGui is downloaded automatically by CMake during configuration. libusb -will be required when the USB layer is implemented. +Dear ImGui is downloaded automatically by CMake during configuration. + +### Hantek DSO-2250 USB Access + +Linux requires a udev rule for a regular desktop user to open the Hantek device +through libusb. Install the supplied rule, reload udev rules, then reconnect the +oscilloscope: + +```bash +sudo cp usb/80-hantek-dso-2250.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules +sudo udevadm trigger +``` ## Build diff --git a/app/main.cpp b/app/main.cpp index cc8ec68..b8b55ed 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,10 +1,23 @@ /** - * @copyright @showdate "%Y " Anton Chernov. All rights reserved. * @file main.cpp * @version 0.2.1 * @authors Anton Chernov * @date 2026-08-28 * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ /********************************* Definition ********************************/ @@ -62,6 +75,9 @@ #include "usb/usb_device.h" using oscilloscope::usb::EScanStatus; +using oscilloscope::usb::SUsbConnection; +using oscilloscope::usb::SUsbConnectionResult; +using oscilloscope::usb::SUsbDeviceInfo; using oscilloscope::usb::SUsbScanResult; /***************************** Private variables *****************************/ @@ -87,10 +103,23 @@ static void drawOscilloscopeGrid( const ImVec2 &position, const ImVec2 &size ); -static std::string formatUsbScanStatus(const SUsbScanResult &scanResult); +static bool isUsbDevicePresent( + const SUsbScanResult &scanResult, + const SUsbDeviceInfo &deviceInfo +); +static std::string formatUsbConnectionStatus( + const SUsbScanResult &scanResult, + const SUsbConnection &connection +); +static std::string formatUsbConnectionError( + const char *operation, + const SUsbConnectionResult &result +); static void updateDemoMode( bool *demoMode, SUsbScanResult *usbScanResult, + SUsbConnection *connection, + SUsbDeviceInfo *connectedDevice, std::string *deviceStatus ); @@ -149,7 +178,12 @@ int main (void) { int voltsPerDivision[] = {2, 2}; SUsbScanResult usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); - std::string deviceStatus = formatUsbScanStatus(usbScanResult); + SUsbConnection usbConnection = {NULL, NULL, 0U, false}; + SUsbDeviceInfo connectedDevice = {0U, 0U, 0U, 0U, NULL}; + std::string deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); const char* timebases[] = { "4 ns/div", "20 ns/div", "100 ns/div", "1 us/div", "10 us/div", "100 us/div", "1 ms/div", "10 ms/div", "100 ms/div", "1 s/div" @@ -186,6 +220,8 @@ int main (void) { updateDemoMode( &demoMode, &usbScanResult, + &usbConnection, + &connectedDevice, &deviceStatus ); } @@ -236,7 +272,85 @@ int main (void) { } if (ImGui::Button("Rescan devices", ImVec2(-1.0f, 32.0f))) { usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); - deviceStatus = formatUsbScanStatus(usbScanResult); + + if ( + usbConnection.isConnected && + !isUsbDevicePresent(usbScanResult, connectedDevice) + ) { + const SUsbConnectionResult disconnectResult = + oscilloscope::usb::disconnectFromDevice(&usbConnection); + + connectedDevice = {0U, 0U, 0U, 0U, NULL}; + if (disconnectResult.errorMessage.empty()) { + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + else { + deviceStatus = formatUsbConnectionError( + "Disconnect", + disconnectResult + ); + } + } + else { + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + } + + if (usbConnection.isConnected) { + if (ImGui::Button("Disconnect", ImVec2(-1.0f, 32.0f))) { + const SUsbConnectionResult disconnectResult = + oscilloscope::usb::disconnectFromDevice(&usbConnection); + + connectedDevice = {0U, 0U, 0U, 0U, NULL}; + if (disconnectResult.errorMessage.empty()) { + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + else { + deviceStatus = formatUsbConnectionError( + "Disconnect", + disconnectResult + ); + } + } + } + else { + const bool canConnect = + !demoMode && + (usbScanResult.status == EScanStatus::eSuccess) && + !usbScanResult.devices.empty(); + + ImGui::BeginDisabled(!canConnect); + if (ImGui::Button("Connect", ImVec2(-1.0f, 32.0f))) { + const SUsbConnectionResult connectResult = + oscilloscope::usb::connectToDevice( + usbScanResult.devices.front(), + &usbConnection + ); + + if (connectResult.errorMessage.empty()) { + connectedDevice = usbScanResult.devices.front(); + deviceStatus = formatUsbConnectionStatus( + usbScanResult, + usbConnection + ); + } + else { + deviceStatus = formatUsbConnectionError( + "Connect", + connectResult + ); + } + } + ImGui::EndDisabled(); } ImGui::Separator(); ImGui::TextUnformatted("Horizontal"); @@ -258,7 +372,13 @@ int main (void) { bool newDemoMode = demoMode; if (ImGui::Checkbox("Demo mode", &newDemoMode)) { - updateDemoMode(&demoMode, &usbScanResult, &deviceStatus); + updateDemoMode( + &demoMode, + &usbScanResult, + &usbConnection, + &connectedDevice, + &deviceStatus + ); } ImGui::EndChild(); @@ -287,6 +407,9 @@ int main (void) { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); + if (usbConnection.isConnected) { + oscilloscope::usb::disconnectFromDevice(&usbConnection); + } SDL_GL_DeleteContext(glContext); SDL_DestroyWindow(window); SDL_Quit(); @@ -294,15 +417,43 @@ int main (void) { } /***************************** Private functions *****************************/ -static std::string formatUsbScanStatus(const SUsbScanResult &scanResult) { +static bool isUsbDevicePresent( + const SUsbScanResult &scanResult, + const SUsbDeviceInfo &deviceInfo +) { + bool isPresent = false; + + for (const SUsbDeviceInfo &device : scanResult.devices) { + if ( + (device.vendorId == deviceInfo.vendorId) && + (device.productId == deviceInfo.productId) && + (device.busNumber == deviceInfo.busNumber) && + (device.deviceAddress == deviceInfo.deviceAddress) + ) { + isPresent = true; + break; + } + } + + return isPresent; +} + +static std::string formatUsbConnectionStatus( + const SUsbScanResult &scanResult, + const SUsbConnection &connection +) { std::string status; - if (scanResult.status == EScanStatus::eSuccess) { + if (connection.isConnected) { + status = "Connected"; + } + else if (scanResult.status == EScanStatus::eSuccess) { if (scanResult.devices.empty()) { - status = "No supported device"; + status = "Disconnected: No supported device"; } else { - status = scanResult.devices.front().modelName; + status = "Disconnected: "; + status += scanResult.devices.front().modelName; status += " detected"; } } @@ -314,16 +465,49 @@ static std::string formatUsbScanStatus(const SUsbScanResult &scanResult) { return status; } +static std::string formatUsbConnectionError( + const char *operation, + const SUsbConnectionResult &result +) { + std::string status = operation; + + status += " error: "; + status += result.errorMessage; + return status; +} + static void updateDemoMode( bool *demoMode, SUsbScanResult *usbScanResult, + SUsbConnection *connection, + SUsbDeviceInfo *connectedDevice, std::string *deviceStatus ) { + bool updateStatus = true; + *demoMode = !*demoMode; - if (!*demoMode) { + if (*demoMode) { + if (connection->isConnected) { + const SUsbConnectionResult disconnectResult = + oscilloscope::usb::disconnectFromDevice(connection); + + *connectedDevice = {0U, 0U, 0U, 0U, NULL}; + if (!disconnectResult.errorMessage.empty()) { + *deviceStatus = formatUsbConnectionError( + "Disconnect", + disconnectResult + ); + updateStatus = false; + } + } + } + else { *usbScanResult = oscilloscope::usb::enumerateSupportedDevices(); - *deviceStatus = formatUsbScanStatus(*usbScanResult); + } + + if (updateStatus) { + *deviceStatus = formatUsbConnectionStatus(*usbScanResult, *connection); } } diff --git a/clear_project.sh b/clear_project.sh index 3ac3269..def8e16 100755 --- a/clear_project.sh +++ b/clear_project.sh @@ -9,7 +9,7 @@ clear out_app="./Output" -out_docs="./docs/Out" +out_docs="./docs/out" pycache="./__pycache__" # Clean application output directory diff --git a/usb/80-hantek-dso-2250.rules b/usb/80-hantek-dso-2250.rules new file mode 100644 index 0000000..162ffd0 --- /dev/null +++ b/usb/80-hantek-dso-2250.rules @@ -0,0 +1,2 @@ +# Permit the active local user to access the Hantek DSO-2250 via libusb. +SUBSYSTEM=="usb", ATTR{idVendor}=="04b4", ATTR{idProduct}=="2250", TAG+="uaccess" diff --git a/usb/usb_device.cpp b/usb/usb_device.cpp index cb49c9b..ae1f637 100644 --- a/usb/usb_device.cpp +++ b/usb/usb_device.cpp @@ -1,16 +1,30 @@ /** - * @copyright @showdate "%Y " Anton Chernov. All rights reserved. * @file usb_device.cpp * @version 0.2.1 * @authors Anton Chernov * @date 2026-09-01 * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ -/******************************* Included files ******************************/ -#include "usb_device.h" +/******************************* Included files *******************************/ #include +#include "usb_device.h" + /********************************* Definitions ********************************/ namespace oscilloscope { @@ -20,14 +34,15 @@ namespace usb { struct SSupportedDevice { uint16_t vendorId; uint16_t productId; - const char* modelName; + uint8_t interfaceNumber; + const char *modelName; }; /****************************** Module variables ******************************/ /** @brief Maps supported USB VID/PID pairs to model display names */ static const SSupportedDevice supported_devices[] = { - {0x04B4U, 0x2250U, "Hantek DSO-2250"} + {0x04B4U, 0x2250U, 0U, "Hantek DSO-2250"} }; /***************************** Private prototypes *****************************/ @@ -43,12 +58,24 @@ static const SSupportedDevice* findSupportedDevice( uint16_t productId ); -/********************* Application Programming Interface *********************/ +/** + * @brief Finds a specific bus/address combination in the libusb device list + * @param[in] deviceList libusb device list to search + * @param[in] deviceInfo Requested device descriptor + * @returns Matching libusb device, or NULL when not found + */ +static libusb_device* findDeviceByInfo( + libusb_device **deviceList, + const SUsbDeviceInfo &deviceInfo, + const ssize_t deviceCount +); + +/********************* Application Programming Interface **********************/ /** @fn enumerateSupportedDevices */ SUsbScanResult enumerateSupportedDevices() { - libusb_context* context = NULL; - libusb_device** deviceList = NULL; + libusb_context *context = NULL; + libusb_device **deviceList = NULL; SUsbScanResult result = {EScanStatus::eSuccess, {}, ""}; const int initializationResult = libusb_init(&context); @@ -72,7 +99,7 @@ SUsbScanResult enumerateSupportedDevices() { ); if (descriptorResult == LIBUSB_SUCCESS) { - const SSupportedDevice* supportedDevice = findSupportedDevice( + const SSupportedDevice *supportedDevice = findSupportedDevice( descriptor.idVendor, descriptor.idProduct ); @@ -99,6 +126,138 @@ SUsbScanResult enumerateSupportedDevices() { return result; } +/** @fn connectToDevice */ +SUsbConnectionResult connectToDevice( + const SUsbDeviceInfo &deviceInfo, + SUsbConnection *connection +) { + SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; + libusb_context *context = NULL; + libusb_device **deviceList = NULL; + libusb_device *device = NULL; + libusb_device_handle *handle = NULL; + const SSupportedDevice *supportedDevice = NULL; + ssize_t deviceCount = 0; + int initializationResult = LIBUSB_SUCCESS; + int openResult = LIBUSB_SUCCESS; + int claimResult = LIBUSB_SUCCESS; + + if (connection == NULL) { + result.status = EConnectionStatus::eDeviceNotFound; + result.errorMessage = "Connection handle is null"; + } + else { + if (connection->isConnected) { + result = disconnectFromDevice(connection); + } + + if (result.status == EConnectionStatus::eDisconnected) { + initializationResult = libusb_init(&context); + + if (initializationResult != LIBUSB_SUCCESS) { + result.status = EConnectionStatus::eInitializationFailed; + result.errorMessage = libusb_error_name(initializationResult); + } + else { + deviceCount = libusb_get_device_list(context, &deviceList); + device = findDeviceByInfo(deviceList, deviceInfo, deviceCount); + supportedDevice = findSupportedDevice( + deviceInfo.vendorId, + deviceInfo.productId + ); + + if ((deviceCount < 0) || (device == NULL) || + (supportedDevice == NULL)) { + result.status = EConnectionStatus::eDeviceNotFound; + result.errorMessage = "Requested USB device was not found"; + } + else { + openResult = libusb_open(device, &handle); + + if (openResult != LIBUSB_SUCCESS) { + result.status = EConnectionStatus::eOpenFailed; + result.errorMessage = libusb_error_name(openResult); + } + else { + claimResult = libusb_claim_interface( + handle, + supportedDevice->interfaceNumber + ); + + if (claimResult != LIBUSB_SUCCESS) { + result.status = EConnectionStatus::eClaimInterfaceFailed; + result.errorMessage = libusb_error_name(claimResult); + } + else { + connection->context = context; + connection->handle = handle; + connection->interfaceNumber = + supportedDevice->interfaceNumber; + connection->isConnected = true; + result.status = EConnectionStatus::eConnected; + context = NULL; + handle = NULL; + } + } + } + } + } + } + + if (deviceList != NULL) { + libusb_free_device_list(deviceList, 1); + } + if (handle != NULL) { + libusb_close(handle); + } + if (context != NULL) { + libusb_exit(context); + } + + return result; +} + +/** @fn disconnectFromDevice */ +SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection) { + SUsbConnectionResult result = {EConnectionStatus::eDisconnected, ""}; + int releaseResult = LIBUSB_SUCCESS; + + if (connection == NULL) { + result.status = EConnectionStatus::eDeviceNotFound; + result.errorMessage = "Connection handle is null"; + } + else { + if ((connection->handle != NULL) && connection->isConnected) { + releaseResult = libusb_release_interface( + connection->handle, + connection->interfaceNumber + ); + + if ( + (releaseResult != LIBUSB_SUCCESS) && + (releaseResult != LIBUSB_ERROR_NO_DEVICE) + ) { + result.status = EConnectionStatus::eReleaseFailed; + result.errorMessage = libusb_error_name(releaseResult); + } + } + + if (connection->handle != NULL) { + libusb_close(connection->handle); + } + if (connection->context != NULL) { + libusb_exit(connection->context); + } + + connection->context = NULL; + connection->handle = NULL; + connection->interfaceNumber = 0U; + connection->isConnected = false; + } + + return result; +} + /****************************** Private functions *****************************/ /** @fn findSupportedDevice */ @@ -106,7 +265,7 @@ static const SSupportedDevice* findSupportedDevice( const uint16_t vendorId, const uint16_t productId ) { - const SSupportedDevice* result = NULL; + const SSupportedDevice *result = NULL; const size_t deviceCount = sizeof(supported_devices) / sizeof(supported_devices[0]); @@ -123,6 +282,40 @@ static const SSupportedDevice* findSupportedDevice( return result; } +/** @fn findDeviceByInfo */ +static libusb_device* findDeviceByInfo( + libusb_device **deviceList, + const SUsbDeviceInfo &deviceInfo, + const ssize_t deviceCount +) { + libusb_device *result = NULL; + libusb_device *candidate = NULL; + libusb_device_descriptor descriptor; + int descriptorResult = LIBUSB_ERROR_OTHER; + + for (ssize_t index = 0; index < deviceCount; ++index) { + candidate = deviceList[index]; + descriptorResult = libusb_get_device_descriptor( + candidate, + &descriptor + ); + + if (descriptorResult == LIBUSB_SUCCESS) { + if ( + (descriptor.idVendor == deviceInfo.vendorId) && + (descriptor.idProduct == deviceInfo.productId) && + (libusb_get_bus_number(candidate) == deviceInfo.busNumber) && + (libusb_get_device_address(candidate) == deviceInfo.deviceAddress) + ) { + result = candidate; + break; + } + } + } + + return result; +} + } // namespace usb } // namespace oscilloscope /******************************************************************************/ diff --git a/usb/usb_device.h b/usb/usb_device.h index 3b1ebb0..a1a40bc 100644 --- a/usb/usb_device.h +++ b/usb/usb_device.h @@ -1,10 +1,23 @@ /** - * @copyright @showdate "%Y " Anton Chernov. All rights reserved. * @file usb_device.h * @version 0.2.1 * @authors Anton Chernov * @date 2026-09-01 * @date @showdate "%Y-%m-%d" + * @par + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ #ifndef USB_DEVICE_H_ @@ -15,6 +28,8 @@ #include #include +#include + /********************************* Definitions ********************************/ namespace oscilloscope { @@ -27,13 +42,24 @@ enum class EScanStatus { eEnumerationFailed /**< libusb device enumeration failed */ }; +/** @brief Describes the outcome of a USB connection lifecycle operation */ +enum class EConnectionStatus { + eConnected, /**< Device opened and interface claimed */ + eDisconnected, /**< Device released and closed */ + eInitializationFailed, /**< libusb context initialization failed */ + eDeviceNotFound, /**< Matching device was not found */ + eOpenFailed, /**< libusb_open failed */ + eClaimInterfaceFailed, /**< libusb_claim_interface failed */ + eReleaseFailed /**< libusb_release_interface failed */ +}; + /** @brief Identifies one supported USB oscilloscope instance */ struct SUsbDeviceInfo { uint16_t vendorId; /**< USB vendor identifier */ uint16_t productId; /**< USB product identifier */ uint8_t busNumber; /**< USB bus number */ uint8_t deviceAddress; /**< Address assigned on the USB bus */ - const char* modelName; /**< Supported model display name */ + const char *modelName; /**< Supported model display name */ }; /** @brief Holds the outcome and matching devices from a USB scan */ @@ -43,6 +69,20 @@ struct SUsbScanResult { std::string errorMessage; /**< libusb error for a failed scan */ }; +/** @brief Describes an active USB connection to a supported device */ +struct SUsbConnection { + libusb_context *context; /**< libusb connection context */ + libusb_device_handle *handle; /**< Open USB device handle */ + uint8_t interfaceNumber; /**< Claimed interface number */ + bool isConnected; /**< True once the interface is claimed */ +}; + +/** @brief Holds the result of a connection lifecycle operation */ +struct SUsbConnectionResult { + EConnectionStatus status; /**< Operation outcome */ + std::string errorMessage; /**< libusb error string when applicable */ +}; + /********************* Application Programming Interface *********************/ /** @@ -51,6 +91,24 @@ struct SUsbScanResult { */ SUsbScanResult enumerateSupportedDevices(); +/** + * @brief Connects to a known supported USB oscilloscope + * @param[in] deviceInfo Device descriptor returned by a scan result + * @param[out] connection Connection state container that is filled in + * @returns Connection status and error message when applicable + */ +SUsbConnectionResult connectToDevice( + const SUsbDeviceInfo &deviceInfo, + SUsbConnection *connection +); + +/** + * @brief Releases and closes a currently connected USB oscilloscope + * @param[in,out] connection Connection state container to reset + * @returns Disconnection status and error message when applicable + */ +SUsbConnectionResult disconnectFromDevice(SUsbConnection *connection); + } // namespace usb } // namespace oscilloscope