feat: add gs_usb support on Windows
candleLight, CANable, CANnectivity and cantact adapters speak gs_usb. On Linux and macOS the kernel driver exposes them as SocketCAN interfaces, so SavvyCAN already reached them through Qt SerialBus. Windows has no such driver, which left those adapters unusable there. Add a GSUSBConnection that talks to the hardware directly over WinUSB: - one connection per physical device, one SavvyCAN bus per CAN channel - CAN FD including BRS, plus RTR, extended IDs and error frames - known good bit timings for 16/48/80/160/170 MHz device clocks, with a generic solver for clocks the tables do not cover - hardware timestamps, unwrapped across the 32 bit rollover and anchored to the host clock so they line up with the rest of SavvyCAN gs_usb multiplexes every channel over a single USB bulk IN endpoint, so a single reader thread drains it and hands frames to the connection thread in batches, coalescing the wakeups. That keeps one producer on the lock free queue, which the tx echo path already writes to. Device scans skip adapters that a live connection holds. candle_dev_open() shares the file handle and queues read URBs immediately, so probing a device in use would consume frames the open connection is waiting for. connections/candle_api is an unmodified copy of the candle Windows API, LGPL 3.0 rather than MIT and only compiled into Windows builds. See its README for provenance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZnuZJ7zc3e8hk6C8bGDZN
This commit is contained in:
@@ -250,6 +250,32 @@ win32-g++ {
|
|||||||
LIBS += libopengl32
|
LIBS += libopengl32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# gs_usb (candleLight / CANable / CANnectivity) support.
|
||||||
|
# Windows only - everywhere else these devices are driven by the kernel gs_usb module and
|
||||||
|
# show up as regular SocketCAN interfaces.
|
||||||
|
# connections/candle_api is LGPL-3.0, see connections/candle_api/README.md.
|
||||||
|
win32 {
|
||||||
|
SOURCES += connections/gs_usb.cpp \
|
||||||
|
connections/candle_api/candle.c \
|
||||||
|
connections/candle_api/candle_ctrl_req.c
|
||||||
|
|
||||||
|
HEADERS += connections/gs_usb.h \
|
||||||
|
connections/candle_api/candle.h \
|
||||||
|
connections/candle_api/candle_ctrl_req.h \
|
||||||
|
connections/candle_api/candle_defs.h \
|
||||||
|
connections/candle_api/ch_9.h
|
||||||
|
|
||||||
|
#lower case names so this also links with a case sensitive MinGW cross toolchain
|
||||||
|
LIBS += -lsetupapi -lole32 -lwinusb
|
||||||
|
|
||||||
|
# the candle API calls the TCHAR flavour of the setupapi/strsafe functions with wide strings
|
||||||
|
DEFINES += UNICODE _UNICODE
|
||||||
|
}
|
||||||
|
|
||||||
|
# the candle API is C99/C11, the rest of the project is C++ only so this affects nothing else
|
||||||
|
win32-g++:QMAKE_CFLAGS += -std=gnu11
|
||||||
|
win32-msvc*:QMAKE_CFLAGS += /std:c11
|
||||||
|
|
||||||
unix {
|
unix {
|
||||||
isEmpty(PREFIX) {
|
isEmpty(PREFIX) {
|
||||||
PREFIX=/usr/local
|
PREFIX=/usr/local
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace CANCon {
|
|||||||
LAWICEL,
|
LAWICEL,
|
||||||
CANSERVER,
|
CANSERVER,
|
||||||
CANLOGSERVER,
|
CANLOGSERVER,
|
||||||
|
GS_USB,
|
||||||
NONE
|
NONE
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "lawicel_serial.h"
|
#include "lawicel_serial.h"
|
||||||
#include "canserver.h"
|
#include "canserver.h"
|
||||||
#include "canlogserver.h"
|
#include "canlogserver.h"
|
||||||
|
#include "gs_usb.h"
|
||||||
|
|
||||||
using namespace CANCon;
|
using namespace CANCon;
|
||||||
|
|
||||||
@@ -32,6 +33,13 @@ CANConnection* CanConFactory::create(type pType, QString pPortName, QString pDri
|
|||||||
return new CANserver(pPortName);
|
return new CANserver(pPortName);
|
||||||
case CANLOGSERVER:
|
case CANLOGSERVER:
|
||||||
return new CanLogServer(pPortName);
|
return new CanLogServer(pPortName);
|
||||||
|
case GS_USB:
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
return new GSUSBConnection(pPortName, pBusSpeed, pCanFd, pDataRate);
|
||||||
|
#else
|
||||||
|
//everywhere else gs_usb devices are handled by the kernel driver and show up as SocketCAN
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
default: {}
|
default: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ QVariant CANConnectionModel::data(const QModelIndex &index, int role) const
|
|||||||
case CANCon::LAWICEL: return "LAWICEL";
|
case CANCon::LAWICEL: return "LAWICEL";
|
||||||
case CANCon::CANSERVER: return "CANserver";
|
case CANCon::CANSERVER: return "CANserver";
|
||||||
case CANCon::CANLOGSERVER: return "CanLogServer";
|
case CANCon::CANLOGSERVER: return "CanLogServer";
|
||||||
|
case CANCon::GS_USB: return "gs_usb";
|
||||||
default: {}
|
default: {}
|
||||||
}
|
}
|
||||||
else qDebug() << "Tried to show connection type but connection was nullptr";
|
else qDebug() << "Tried to show connection type but connection was nullptr";
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below.
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”.
|
||||||
|
|
||||||
|
The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL.
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License.
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License.
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license document.
|
||||||
|
c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document.
|
||||||
|
d) Do one of the following:
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.
|
||||||
|
1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version.
|
||||||
|
e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.)
|
||||||
|
|
||||||
|
5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License.
|
||||||
|
b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# candle API (gs_usb / candleLight Windows driver)
|
||||||
|
|
||||||
|
Vendored, unmodified copy of the candle Windows API used by SavvyCAN's gs_usb
|
||||||
|
connection (`connections/gs_usb.cpp`). It talks to gs_usb class devices
|
||||||
|
(candleLight, CANable, CANnectivity, cantact, ...) over WinUSB, so no vendor
|
||||||
|
driver is needed — only a WinUSB binding on the device interface (which the
|
||||||
|
stock candleLight/CANable firmware already declares via WCID descriptors).
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Original: <https://github.com/HubertD/candle_dll> — Copyright (c) 2016 Hubert Denkmair
|
||||||
|
- This copy: <https://github.com/Schildkroet/CANgaroo>, `src/driver/CandleApiDriver/api/`
|
||||||
|
at commit `ed3088d5393e02a01afe867af2ee8ab0e12db039`, which adds CAN FD,
|
||||||
|
multi-channel and hardware timestamp support — Copyright (c) 2026 Schildkroet
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
These files are **LGPL-3.0**, not MIT like the rest of SavvyCAN. They are only
|
||||||
|
compiled into Windows builds. Keep `LICENSE` alongside them, and keep the files
|
||||||
|
separable so the LGPL relinking provision can be satisfied. Do not copy code out
|
||||||
|
of this directory into the MIT-licensed parts of the tree.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||||
|
Copyright (c) 2026 Schildkroet
|
||||||
|
|
||||||
|
This file is part of the candle windows API.
|
||||||
|
|
||||||
|
This library is free software: you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation, either
|
||||||
|
version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef void* candle_list_handle;
|
||||||
|
typedef void* candle_handle;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_DEVSTATE_AVAIL,
|
||||||
|
CANDLE_DEVSTATE_INUSE
|
||||||
|
} candle_devstate_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_FRAMETYPE_UNKNOWN,
|
||||||
|
CANDLE_FRAMETYPE_RECEIVE,
|
||||||
|
CANDLE_FRAMETYPE_ECHO,
|
||||||
|
CANDLE_FRAMETYPE_ERROR,
|
||||||
|
CANDLE_FRAMETYPE_TIMESTAMP_OVFL
|
||||||
|
} candle_frametype_t;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CANDLE_ID_EXTENDED = 0x80000000,
|
||||||
|
CANDLE_ID_RTR = 0x40000000,
|
||||||
|
CANDLE_ID_ERR = 0x20000000
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Feature flags reported in candle_capability_t.feature */
|
||||||
|
enum {
|
||||||
|
/** CAN channel supports listen-only mode, in which it is not allowed to send dominant bits. */
|
||||||
|
CANDLE_FEATURE_LISTEN_ONLY = 0x0001,
|
||||||
|
/** CAN channel supports loopback mode, in which it receives own frames. */
|
||||||
|
CANDLE_FEATURE_LOOP_BACK = 0x0002,
|
||||||
|
/** CAN channel supports triple sampling mode */
|
||||||
|
CANDLE_FEATURE_TRIPLE_SAMPLE = 0x0004,
|
||||||
|
/** CAN channel supports not retransmitting in case of lost arbitration or missing ACK. */
|
||||||
|
CANDLE_FEATURE_ONE_SHOT = 0x0008,
|
||||||
|
/** CAN channel supports hardware timestamping of CAN frames. */
|
||||||
|
CANDLE_FEATURE_HW_TIMESTAMP = 0x0010,
|
||||||
|
/** CAN channel supports visual identification. */
|
||||||
|
CANDLE_FEATURE_IDENTIFY = 0x0020,
|
||||||
|
/** CAN channel supports user IDs (unsupported). */
|
||||||
|
CANDLE_FEATURE_USER_ID = 0x0040,
|
||||||
|
/** CAN channel supports padding of host frames (unsupported). */
|
||||||
|
CANDLE_FEATURE_PAD_PKTS_TO_MAX = 0x0080,
|
||||||
|
/** CAN channel supports transmitting/receiving CAN FD frames. */
|
||||||
|
CANDLE_FEATURE_FD = 0x0100,
|
||||||
|
/** CAN channel support LPC546xx specific quirks (Unused) */
|
||||||
|
CANDLE_FEATURE_REQ_USB_QUIRK_LPC546XX = 0x0200,
|
||||||
|
/** CAN channel supports extended bit timing limits. */
|
||||||
|
CANDLE_FEATURE_BT_CONST_EXT = 0x0400,
|
||||||
|
/** CAN channel supports configurable bus termination. */
|
||||||
|
CANDLE_FEATURE_TERMINATION = 0x0800,
|
||||||
|
/** CAN channel supports bus error reporting (Unsupported, always enabled) */
|
||||||
|
CANDLE_FEATURE_BERR_REPORTING = 0x1000,
|
||||||
|
/** CAN channel supports reporting of bus state. */
|
||||||
|
CANDLE_FEATURE_GET_STATE = 0x2000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Flags in the flags byte of received/transmitted frames */
|
||||||
|
enum {
|
||||||
|
CANDLE_FRAME_FLAG_OVERFLOW = 0x01,
|
||||||
|
CANDLE_FRAME_FLAG_FD = 0x02,
|
||||||
|
CANDLE_FRAME_FLAG_BRS = 0x04,
|
||||||
|
CANDLE_FRAME_FLAG_ESI = 0x08,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_STATE_ERROR_ACTIVE = 0,
|
||||||
|
CANDLE_STATE_ERROR_WARNING = 1,
|
||||||
|
CANDLE_STATE_ERROR_PASSIVE = 2,
|
||||||
|
CANDLE_STATE_BUS_OFF = 3,
|
||||||
|
CANDLE_STATE_STOPPED = 4,
|
||||||
|
CANDLE_STATE_SLEEPING = 5,
|
||||||
|
} candle_can_state_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_MODE_NORMAL = 0x0000,
|
||||||
|
CANDLE_MODE_LISTEN_ONLY = 0x0001,
|
||||||
|
CANDLE_MODE_LOOP_BACK = 0x0002,
|
||||||
|
CANDLE_MODE_TRIPLE_SAMPLE = 0x0004,
|
||||||
|
CANDLE_MODE_ONE_SHOT = 0x0008,
|
||||||
|
CANDLE_MODE_HW_TIMESTAMP = 0x0010,
|
||||||
|
CANDLE_MODE_PAD_PKTS_TO_MAX = 0x0080,
|
||||||
|
CANDLE_MODE_FD = 0x0100,
|
||||||
|
} candle_mode_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANDLE_ERR_OK = 0,
|
||||||
|
CANDLE_ERR_CREATE_FILE = 1,
|
||||||
|
CANDLE_ERR_WINUSB_INITIALIZE = 2,
|
||||||
|
CANDLE_ERR_QUERY_INTERFACE = 3,
|
||||||
|
CANDLE_ERR_QUERY_PIPE = 4,
|
||||||
|
CANDLE_ERR_PARSE_IF_DESCR = 5,
|
||||||
|
CANDLE_ERR_SET_HOST_FORMAT = 6,
|
||||||
|
CANDLE_ERR_GET_DEVICE_INFO = 7,
|
||||||
|
CANDLE_ERR_GET_BITTIMING_CONST = 8,
|
||||||
|
CANDLE_ERR_PREPARE_READ = 9,
|
||||||
|
CANDLE_ERR_SET_DEVICE_MODE = 10,
|
||||||
|
CANDLE_ERR_SET_BITTIMING = 11,
|
||||||
|
CANDLE_ERR_BITRATE_FCLK = 12,
|
||||||
|
CANDLE_ERR_BITRATE_UNSUPPORTED = 13,
|
||||||
|
CANDLE_ERR_SEND_FRAME = 14,
|
||||||
|
CANDLE_ERR_READ_TIMEOUT = 15,
|
||||||
|
CANDLE_ERR_READ_WAIT = 16,
|
||||||
|
CANDLE_ERR_READ_RESULT = 17,
|
||||||
|
CANDLE_ERR_READ_SIZE = 18,
|
||||||
|
CANDLE_ERR_SETUPDI_IF_DETAILS = 19,
|
||||||
|
CANDLE_ERR_SETUPDI_IF_DETAILS2 = 20,
|
||||||
|
CANDLE_ERR_MALLOC = 21,
|
||||||
|
CANDLE_ERR_PATH_LEN = 22,
|
||||||
|
CANDLE_ERR_CLSID = 23,
|
||||||
|
CANDLE_ERR_GET_DEVICES = 24,
|
||||||
|
CANDLE_ERR_SETUPDI_IF_ENUM = 25,
|
||||||
|
CANDLE_ERR_SET_TIMESTAMP_MODE = 26,
|
||||||
|
CANDLE_ERR_DEV_OUT_OF_RANGE = 27,
|
||||||
|
CANDLE_ERR_GET_TIMESTAMP = 28,
|
||||||
|
CANDLE_ERR_SET_PIPE_RAW_IO = 29
|
||||||
|
} candle_err_t;
|
||||||
|
|
||||||
|
#pragma pack(push,1)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t echo_id;
|
||||||
|
uint32_t can_id;
|
||||||
|
uint8_t can_dlc;
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t flags;
|
||||||
|
uint8_t reserved;
|
||||||
|
uint8_t data[8];
|
||||||
|
uint32_t timestamp_us;
|
||||||
|
} candle_frame_t;
|
||||||
|
|
||||||
|
/* CAN FD frame: same header as candle_frame_t but with 64-byte data payload */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t echo_id;
|
||||||
|
uint32_t can_id;
|
||||||
|
uint8_t can_dlc;
|
||||||
|
uint8_t channel;
|
||||||
|
uint8_t flags;
|
||||||
|
uint8_t reserved;
|
||||||
|
uint8_t data[64];
|
||||||
|
uint32_t timestamp_us;
|
||||||
|
} candle_fd_frame_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t feature;
|
||||||
|
uint32_t fclk_can;
|
||||||
|
uint32_t tseg1_min;
|
||||||
|
uint32_t tseg1_max;
|
||||||
|
uint32_t tseg2_min;
|
||||||
|
uint32_t tseg2_max;
|
||||||
|
uint32_t sjw_max;
|
||||||
|
uint32_t brp_min;
|
||||||
|
uint32_t brp_max;
|
||||||
|
uint32_t brp_inc;
|
||||||
|
} candle_capability_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t prop_seg;
|
||||||
|
uint32_t phase_seg1;
|
||||||
|
uint32_t phase_seg2;
|
||||||
|
uint32_t sjw;
|
||||||
|
uint32_t brp;
|
||||||
|
} candle_bittiming_t;
|
||||||
|
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CAN FD DLC encoding:
|
||||||
|
* DLC 0-8 → 0-8 bytes (same as classic CAN)
|
||||||
|
* DLC 9 → 12 bytes
|
||||||
|
* DLC 10 → 16 bytes
|
||||||
|
* DLC 11 → 20 bytes
|
||||||
|
* DLC 12 → 24 bytes
|
||||||
|
* DLC 13 → 32 bytes
|
||||||
|
* DLC 14 → 48 bytes
|
||||||
|
* DLC 15 → 64 bytes
|
||||||
|
*/
|
||||||
|
static inline uint8_t candle_dlc_to_len(uint8_t dlc)
|
||||||
|
{
|
||||||
|
static const uint8_t tbl[16] = { 0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64 };
|
||||||
|
return (dlc <= 15u) ? tbl[dlc] : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint8_t candle_len_to_dlc(uint8_t len)
|
||||||
|
{
|
||||||
|
if (len <= 8u) return len;
|
||||||
|
if (len <= 12u) return 9u;
|
||||||
|
if (len <= 16u) return 10u;
|
||||||
|
if (len <= 20u) return 11u;
|
||||||
|
if (len <= 24u) return 12u;
|
||||||
|
if (len <= 32u) return 13u;
|
||||||
|
if (len <= 48u) return 14u;
|
||||||
|
return 15u;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DLL
|
||||||
|
|
||||||
|
/* Optional log callback — set once at startup to receive diagnostic messages.
|
||||||
|
* If NULL (the default) no logging is performed. */
|
||||||
|
typedef void (*candle_log_fn_t)(const wchar_t *msg);
|
||||||
|
extern candle_log_fn_t candle_log_fn;
|
||||||
|
|
||||||
|
/* Set to true to enable per-frame and per-control-transfer trace logs.
|
||||||
|
* Off by default; only error and setup messages are logged. */
|
||||||
|
extern bool candle_log_verbose;
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_list_scan(candle_list_handle *list);
|
||||||
|
bool __stdcall DLL candle_list_free(candle_list_handle list);
|
||||||
|
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
|
||||||
|
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
|
||||||
|
wchar_t __stdcall DLL *candle_dev_get_path(candle_handle hdev);
|
||||||
|
bool __stdcall DLL candle_dev_open(candle_handle hdev);
|
||||||
|
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
|
||||||
|
bool __stdcall DLL candle_dev_close(candle_handle hdev);
|
||||||
|
bool __stdcall DLL candle_dev_free(candle_handle hdev);
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels);
|
||||||
|
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap);
|
||||||
|
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state);
|
||||||
|
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
|
||||||
|
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
|
||||||
|
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
|
||||||
|
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
|
||||||
|
|
||||||
|
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);
|
||||||
|
|
||||||
|
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame);
|
||||||
|
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame);
|
||||||
|
uint8_t __stdcall DLL *candle_frame_data(candle_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame);
|
||||||
|
|
||||||
|
/* CAN FD extensions */
|
||||||
|
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data);
|
||||||
|
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms);
|
||||||
|
|
||||||
|
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame);
|
||||||
|
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame);
|
||||||
|
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame);
|
||||||
|
uint8_t __stdcall DLL *candle_fd_frame_data(candle_fd_frame_t *frame);
|
||||||
|
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame);
|
||||||
|
|
||||||
|
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||||
|
|
||||||
|
This file is part of the candle windows API.
|
||||||
|
|
||||||
|
This library is free software: you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation, either
|
||||||
|
version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "candle_ctrl_req.h"
|
||||||
|
#include "ch_9.h"
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CANDLE_BREQ_HOST_FORMAT = 0,
|
||||||
|
CANDLE_BREQ_BITTIMING = 1,
|
||||||
|
CANDLE_BREQ_MODE = 2,
|
||||||
|
CANDLE_BREQ_BERR = 3,
|
||||||
|
CANDLE_BREQ_BT_CONST = 4,
|
||||||
|
CANDLE_BREQ_DEVICE_CONFIG = 5,
|
||||||
|
CANDLE_TIMESTAMP_GET = 6,
|
||||||
|
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
|
||||||
|
CANDLE_BREQ_DATA_BITTIMING = 10,
|
||||||
|
/* 11: SET_TERMINATION, not used here */
|
||||||
|
CANDLE_BREQ_GET_STATE = 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void candle_ctrl_logf(const wchar_t *fmt, ...)
|
||||||
|
{
|
||||||
|
if (candle_log_fn == NULL || !candle_log_verbose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t buf[512];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
HRESULT hr = StringCchVPrintfW(buf, 512, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
candle_log_fn(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool usb_control_msg(WINUSB_INTERFACE_HANDLE hnd, uint8_t request, uint8_t requesttype, uint16_t value, uint16_t index, void *data, uint16_t size)
|
||||||
|
{
|
||||||
|
WINUSB_SETUP_PACKET packet;
|
||||||
|
memset(&packet, 0, sizeof(packet));
|
||||||
|
|
||||||
|
packet.Request = request;
|
||||||
|
packet.RequestType = requesttype;
|
||||||
|
packet.Value = value;
|
||||||
|
packet.Index = index;
|
||||||
|
packet.Length = size;
|
||||||
|
|
||||||
|
unsigned long bytes_sent = 0;
|
||||||
|
BOOL rc = WinUsb_ControlTransfer(hnd, packet, (uint8_t*)data, size, &bytes_sent, 0);
|
||||||
|
candle_ctrl_logf(L"ctrl req=0x%02x type=0x%02x value=%u index=%u size=%u rc=%u transferred=%lu winerr=%lu",
|
||||||
|
request,
|
||||||
|
requesttype,
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
size,
|
||||||
|
rc ? 1 : 0,
|
||||||
|
bytes_sent,
|
||||||
|
rc ? 0 : GetLastError());
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_host_format(candle_device_t *dev)
|
||||||
|
{
|
||||||
|
candle_host_config_t hconf;
|
||||||
|
hconf.byte_order = 0x0000beef;
|
||||||
|
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_HOST_FORMAT,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
1,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
&hconf,
|
||||||
|
sizeof(hconf)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_HOST_FORMAT;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_device_mode(candle_device_t *dev, uint8_t channel, uint32_t mode, uint32_t flags)
|
||||||
|
{
|
||||||
|
candle_device_mode_t dm;
|
||||||
|
dm.mode = mode;
|
||||||
|
dm.flags = flags;
|
||||||
|
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_MODE,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
&dm,
|
||||||
|
sizeof(dm)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool candle_ctrl_get_config(candle_device_t *dev, candle_device_config_t *dconf)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_DEVICE_CONFIG,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
1,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
dconf,
|
||||||
|
sizeof(*dconf)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_TIMESTAMP_GET,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
1,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
current_timestamp,
|
||||||
|
sizeof(*current_timestamp)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_TIMESTAMP;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_get_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_BT_CONST,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
0,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_BITTIMING,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
0,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_DATA_BITTIMING,
|
||||||
|
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
0,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_BITTIMING;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data)
|
||||||
|
{
|
||||||
|
bool rc = usb_control_msg(
|
||||||
|
dev->winUSBHandle,
|
||||||
|
CANDLE_BREQ_GET_STATE,
|
||||||
|
USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||||
|
channel,
|
||||||
|
dev->interfaceNumber,
|
||||||
|
data,
|
||||||
|
sizeof(*data)
|
||||||
|
);
|
||||||
|
|
||||||
|
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_DEVICE_INFO;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||||
|
Copyright (c) 2026 Schildkroet
|
||||||
|
|
||||||
|
This file is part of the candle windows API.
|
||||||
|
|
||||||
|
This library is free software: you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation, either
|
||||||
|
version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "candle_defs.h"
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CANDLE_DEVMODE_RESET = 0,
|
||||||
|
CANDLE_DEVMODE_START = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
typedef struct {
|
||||||
|
uint32_t state;
|
||||||
|
uint32_t rxerr;
|
||||||
|
uint32_t txerr;
|
||||||
|
} candle_device_state_t;
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
bool candle_ctrl_set_host_format(candle_device_t *dev);
|
||||||
|
bool candle_ctrl_set_device_mode(candle_device_t *dev, uint8_t channel, uint32_t mode, uint32_t flags);
|
||||||
|
bool candle_ctrl_get_config(candle_device_t *dev, candle_device_config_t *dconf);
|
||||||
|
bool candle_ctrl_get_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *data);
|
||||||
|
bool candle_ctrl_set_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data);
|
||||||
|
bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candle_bittiming_t *data);
|
||||||
|
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp);
|
||||||
|
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data);
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||||
|
Copyright (c) 2026 Schildkroet
|
||||||
|
|
||||||
|
This file is part of the candle windows API.
|
||||||
|
|
||||||
|
This library is free software: you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation, either
|
||||||
|
version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifndef _WIN32_WINNT
|
||||||
|
#define _WIN32_WINNT 0x0600
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <winbase.h>
|
||||||
|
#include <winusb.h>
|
||||||
|
#include <setupapi.h>
|
||||||
|
#include <devguid.h>
|
||||||
|
#include <regstr.h>
|
||||||
|
|
||||||
|
#undef __CRT__NO_INLINE
|
||||||
|
#include <strsafe.h>
|
||||||
|
#define __CRT__NO_INLINE
|
||||||
|
|
||||||
|
#include "candle.h"
|
||||||
|
|
||||||
|
#define CANDLE_MAX_DEVICES 32
|
||||||
|
#define CANDLE_URB_COUNT 30
|
||||||
|
|
||||||
|
#pragma pack(push,1)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t byte_order;
|
||||||
|
} candle_host_config_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t reserved1;
|
||||||
|
uint8_t reserved2;
|
||||||
|
uint8_t reserved3;
|
||||||
|
uint8_t icount;
|
||||||
|
uint32_t sw_version;
|
||||||
|
uint32_t hw_version;
|
||||||
|
} candle_device_config_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t mode;
|
||||||
|
uint32_t flags;
|
||||||
|
} candle_device_mode_t;
|
||||||
|
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
OVERLAPPED ovl;
|
||||||
|
bool pending;
|
||||||
|
uint8_t buf[512];
|
||||||
|
} canlde_rx_urb;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
wchar_t path[256];
|
||||||
|
candle_devstate_t state;
|
||||||
|
candle_err_t last_error;
|
||||||
|
|
||||||
|
HANDLE deviceHandle;
|
||||||
|
WINUSB_INTERFACE_HANDLE winUSBHandle;
|
||||||
|
UCHAR interfaceNumber;
|
||||||
|
UCHAR bulkInPipe;
|
||||||
|
UCHAR bulkOutPipe;
|
||||||
|
HANDLE txEvent; /* pre-allocated event for timed overlapped writes */
|
||||||
|
|
||||||
|
candle_device_config_t dconf;
|
||||||
|
candle_capability_t bt_const;
|
||||||
|
/* Per-channel capabilities: index 0..dconf.icount, maximum 8 channels */
|
||||||
|
candle_capability_t ch_caps[8];
|
||||||
|
canlde_rx_urb rxurbs[CANDLE_URB_COUNT];
|
||||||
|
HANDLE rxevents[CANDLE_URB_COUNT];
|
||||||
|
} candle_device_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t num_devices;
|
||||||
|
candle_err_t last_error;
|
||||||
|
candle_device_t dev[CANDLE_MAX_DEVICES];
|
||||||
|
} candle_list_t;
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2016 Hubert Denkmair <hubert@denkmair.de>
|
||||||
|
|
||||||
|
This file is part of the candle windows API.
|
||||||
|
|
||||||
|
This library is free software: you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation, either
|
||||||
|
version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library 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
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define USB_DIR_OUT 0 /* to device */
|
||||||
|
#define USB_DIR_IN 0x80 /* to host */
|
||||||
|
|
||||||
|
#define USB_TYPE_MASK (0x03 << 5)
|
||||||
|
#define USB_TYPE_STANDARD (0x00 << 5)
|
||||||
|
#define USB_TYPE_CLASS (0x01 << 5)
|
||||||
|
#define USB_TYPE_VENDOR (0x02 << 5)
|
||||||
|
#define USB_TYPE_RESERVED (0x03 << 5)
|
||||||
|
|
||||||
|
#define USB_RECIP_MASK 0x1f
|
||||||
|
#define USB_RECIP_DEVICE 0x00
|
||||||
|
#define USB_RECIP_INTERFACE 0x01
|
||||||
|
#define USB_RECIP_ENDPOINT 0x02
|
||||||
|
#define USB_RECIP_OTHER 0x03
|
||||||
@@ -426,7 +426,8 @@ void ConnectionWindow::populateBusDetails(int offset)
|
|||||||
//ui->lblBusNum->setText(QString::number(busBase + offset));
|
//ui->lblBusNum->setText(QString::number(busBase + offset));
|
||||||
ui->ckListenOnly->setChecked(bus.isListenOnly());
|
ui->ckListenOnly->setChecked(bus.isListenOnly());
|
||||||
ui->ckEnable->setChecked(bus.isActive());
|
ui->ckEnable->setChecked(bus.isActive());
|
||||||
if (conn_p->getType() == CANCon::type::SERIALBUS || conn_p->getType() == CANCon::type::LAWICEL)
|
if (conn_p->getType() == CANCon::type::SERIALBUS || conn_p->getType() == CANCon::type::LAWICEL
|
||||||
|
|| conn_p->getType() == CANCon::type::GS_USB)
|
||||||
{
|
{
|
||||||
ui->canFDEnable->setVisible(true);
|
ui->canFDEnable->setVisible(true);
|
||||||
ui->canFDEnable_label->setVisible(true);
|
ui->canFDEnable_label->setVisible(true);
|
||||||
|
|||||||
@@ -0,0 +1,980 @@
|
|||||||
|
#include "gs_usb.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMutexLocker>
|
||||||
|
#include <QStringBuilder>
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QSet>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cwchar>
|
||||||
|
#include <cwctype>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/* how long the reader thread blocks on the USB endpoint before re-checking whether it should exit */
|
||||||
|
static const uint32_t GSUSB_READ_TIMEOUT_MS = 50;
|
||||||
|
|
||||||
|
/* gs_usb devices are limited to 8 channels by the candle API */
|
||||||
|
static const int GSUSB_MAX_CHANNELS = 8;
|
||||||
|
|
||||||
|
/* SavvyCAN has no sample point selector, these are the values the CAN and CAN FD specs recommend */
|
||||||
|
static const quint32 GSUSB_NOMINAL_SAMPLE_POINT = 875;
|
||||||
|
static const quint32 GSUSB_DATA_SAMPLE_POINT = 800;
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
/* bit timing */
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A known good bit timing for one device clock / bitrate combination
|
||||||
|
* @note The candle API always sends prop_seg = 1 and sjw = 1, so one bit is
|
||||||
|
* 1 (sync) + 1 (prop) + phaseSeg1 + phaseSeg2 time quanta.
|
||||||
|
*/
|
||||||
|
struct GSUSBTiming
|
||||||
|
{
|
||||||
|
quint32 fclk;
|
||||||
|
quint32 bitrate;
|
||||||
|
quint32 brp;
|
||||||
|
quint32 phaseSeg1;
|
||||||
|
quint32 phaseSeg2;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Nominal (arbitration phase) timings at ~87.5% sample point, taken from cangaroo's
|
||||||
|
* CandleApiInterface. Clocks not listed here fall through to calcBitTiming(). */
|
||||||
|
static const GSUSBTiming GSUSB_NOMINAL_TIMINGS[] = {
|
||||||
|
/* 170 MHz - CANable 2.0 (STM32G0B1) */
|
||||||
|
{ 170000000, 10000, 68, 217, 31 },
|
||||||
|
{ 170000000, 20000, 34, 217, 31 },
|
||||||
|
{ 170000000, 50000, 17, 173, 25 },
|
||||||
|
{ 170000000, 83333, 8, 221, 32 },
|
||||||
|
{ 170000000, 100000, 10, 147, 21 },
|
||||||
|
{ 170000000, 125000, 8, 147, 21 },
|
||||||
|
{ 170000000, 250000, 4, 147, 21 },
|
||||||
|
{ 170000000, 500000, 2, 147, 21 },
|
||||||
|
{ 170000000, 1000000, 1, 147, 21 },
|
||||||
|
|
||||||
|
/* 160 MHz - CANable 2.5 (STM32G4) */
|
||||||
|
{ 160000000, 10000, 80, 173, 25 },
|
||||||
|
{ 160000000, 20000, 40, 173, 25 },
|
||||||
|
{ 160000000, 50000, 16, 173, 25 },
|
||||||
|
{ 160000000, 83333, 8, 208, 30 },
|
||||||
|
{ 160000000, 100000, 10, 138, 20 },
|
||||||
|
{ 160000000, 125000, 8, 138, 20 },
|
||||||
|
{ 160000000, 250000, 4, 138, 20 },
|
||||||
|
{ 160000000, 500000, 2, 138, 20 },
|
||||||
|
{ 160000000, 1000000, 1, 138, 20 },
|
||||||
|
|
||||||
|
/* 80 MHz - CANnectivity */
|
||||||
|
{ 80000000, 10000, 80, 85, 13 },
|
||||||
|
{ 80000000, 20000, 40, 85, 13 },
|
||||||
|
{ 80000000, 50000, 16, 85, 13 },
|
||||||
|
{ 80000000, 100000, 8, 85, 13 },
|
||||||
|
{ 80000000, 125000, 5, 110, 16 },
|
||||||
|
{ 80000000, 250000, 2, 138, 20 },
|
||||||
|
{ 80000000, 500000, 1, 138, 20 },
|
||||||
|
{ 80000000, 800000, 1, 85, 13 },
|
||||||
|
{ 80000000, 1000000, 1, 68, 10 },
|
||||||
|
|
||||||
|
/* 48 MHz - candleLight / CANable 0.x (STM32F072) */
|
||||||
|
{ 48000000, 10000, 300, 12, 2 },
|
||||||
|
{ 48000000, 20000, 150, 12, 2 },
|
||||||
|
{ 48000000, 50000, 60, 12, 2 },
|
||||||
|
{ 48000000, 83333, 36, 12, 2 },
|
||||||
|
{ 48000000, 100000, 30, 12, 2 },
|
||||||
|
{ 48000000, 125000, 24, 12, 2 },
|
||||||
|
{ 48000000, 250000, 12, 12, 2 },
|
||||||
|
{ 48000000, 500000, 6, 12, 2 },
|
||||||
|
{ 48000000, 800000, 4, 11, 2 },
|
||||||
|
{ 48000000, 1000000, 3, 12, 2 },
|
||||||
|
|
||||||
|
/* 16 MHz */
|
||||||
|
{ 16000000, 20000, 50, 12, 2 },
|
||||||
|
{ 16000000, 50000, 20, 12, 2 },
|
||||||
|
{ 16000000, 83333, 12, 12, 2 },
|
||||||
|
{ 16000000, 100000, 10, 12, 2 },
|
||||||
|
{ 16000000, 125000, 8, 12, 2 },
|
||||||
|
{ 16000000, 250000, 4, 12, 2 },
|
||||||
|
{ 16000000, 500000, 2, 12, 2 },
|
||||||
|
{ 16000000, 800000, 1, 16, 2 },
|
||||||
|
{ 16000000, 1000000, 1, 12, 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/* CAN FD data phase timings. The data phase has much tighter hardware limits than the
|
||||||
|
* arbitration phase (tseg1 <= 31, tseg2 <= 16, brp <= 32) which is why the low data rates
|
||||||
|
* need a larger prescaler. */
|
||||||
|
static const GSUSBTiming GSUSB_DATA_TIMINGS[] = {
|
||||||
|
/* 170 MHz */
|
||||||
|
{ 170000000, 1000000, 5, 25, 7 },
|
||||||
|
{ 170000000, 2000000, 5, 12, 3 },
|
||||||
|
{ 170000000, 5000000, 1, 25, 7 },
|
||||||
|
{ 170000000, 10000000, 1, 10, 5 },
|
||||||
|
|
||||||
|
/* 160 MHz */
|
||||||
|
{ 160000000, 1000000, 4, 30, 8 },
|
||||||
|
{ 160000000, 2000000, 2, 30, 8 },
|
||||||
|
{ 160000000, 4000000, 1, 30, 8 },
|
||||||
|
{ 160000000, 5000000, 1, 24, 6 },
|
||||||
|
{ 160000000, 8000000, 1, 14, 4 },
|
||||||
|
|
||||||
|
/* 80 MHz */
|
||||||
|
{ 80000000, 1000000, 2, 30, 8 },
|
||||||
|
{ 80000000, 2000000, 1, 30, 8 },
|
||||||
|
{ 80000000, 4000000, 1, 14, 4 },
|
||||||
|
{ 80000000, 5000000, 1, 11, 3 },
|
||||||
|
{ 80000000, 8000000, 1, 6, 2 },
|
||||||
|
|
||||||
|
/* 48 MHz */
|
||||||
|
{ 48000000, 1000000, 2, 17, 5 },
|
||||||
|
{ 48000000, 2000000, 1, 17, 5 },
|
||||||
|
{ 48000000, 3000000, 1, 11, 3 },
|
||||||
|
{ 48000000, 4000000, 1, 8, 2 },
|
||||||
|
|
||||||
|
/* 16 MHz */
|
||||||
|
{ 16000000, 1000000, 1, 10, 4 },
|
||||||
|
{ 16000000, 2000000, 1, 4, 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Look up a known good timing for a device clock / bitrate pair
|
||||||
|
* @return true if an entry exists, false if the caller has to compute one
|
||||||
|
*/
|
||||||
|
static bool lookupBitTiming(const GSUSBTiming* pTable, size_t pCount, quint32 pFclk,
|
||||||
|
quint32 pBitrate, candle_bittiming_t& pTiming)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < pCount; i++)
|
||||||
|
{
|
||||||
|
if (pTable[i].fclk != pFclk || pTable[i].bitrate != pBitrate) continue;
|
||||||
|
|
||||||
|
pTiming.prop_seg = 1;
|
||||||
|
pTiming.sjw = 1;
|
||||||
|
pTiming.phase_seg1 = pTable[i].phaseSeg1;
|
||||||
|
pTiming.phase_seg2 = pTable[i].phaseSeg2;
|
||||||
|
pTiming.brp = pTable[i].brp;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compute a bit timing for clocks and bitrates the tables do not cover
|
||||||
|
* @param pCaps: capabilities as reported by the device, supplies the clock and the segment limits
|
||||||
|
* @param pDataPhase: true when computing the CAN FD data phase, which has tighter fixed limits
|
||||||
|
* @note This mirrors what the Linux kernel does in can_calc_bittiming(): walk the prescaler
|
||||||
|
* range and keep the candidate with the smallest bitrate error, breaking ties on how
|
||||||
|
* close the sample point lands to the requested one.
|
||||||
|
*/
|
||||||
|
static bool calcBitTiming(const candle_capability_t& pCaps, quint32 pBitrate,
|
||||||
|
quint32 pSamplePoint, bool pDataPhase, candle_bittiming_t& pTiming)
|
||||||
|
{
|
||||||
|
if (pBitrate == 0 || pCaps.fclk_can == 0) return false;
|
||||||
|
|
||||||
|
/* the device only reports arbitration phase limits, the data phase limits are fixed by the spec */
|
||||||
|
const quint32 tseg1Min = pDataPhase ? 2u : std::max(pCaps.tseg1_min, 2u);
|
||||||
|
const quint32 tseg1Max = pDataPhase ? 31u : std::max(pCaps.tseg1_max, 2u);
|
||||||
|
const quint32 tseg2Min = pDataPhase ? 1u : std::max(pCaps.tseg2_min, 1u);
|
||||||
|
const quint32 tseg2Max = pDataPhase ? 16u : std::max(pCaps.tseg2_max, 1u);
|
||||||
|
const quint32 brpMin = std::max(pCaps.brp_min, 1u);
|
||||||
|
const quint32 brpMax = pDataPhase ? std::min(std::max(pCaps.brp_max, 1u), 32u)
|
||||||
|
: std::max(pCaps.brp_max, 1u);
|
||||||
|
const quint32 brpInc = std::max(pCaps.brp_inc, 1u);
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
quint64 bestScore = 0;
|
||||||
|
|
||||||
|
for (quint32 brp = brpMin; brp <= brpMax; brp += brpInc)
|
||||||
|
{
|
||||||
|
const quint32 divisor = brp * pBitrate;
|
||||||
|
if (divisor == 0) continue;
|
||||||
|
|
||||||
|
/* time quanta per bit, rounded to nearest */
|
||||||
|
quint32 tqPerBit = (pCaps.fclk_can + divisor / 2) / divisor;
|
||||||
|
if (tqPerBit < (1 + tseg1Min + tseg2Min)) break; /* larger prescalers only get worse */
|
||||||
|
if (tqPerBit > (1 + tseg1Max + tseg2Max)) continue;
|
||||||
|
|
||||||
|
/* sync segment is always one time quantum */
|
||||||
|
quint32 tseg1 = (tqPerBit * pSamplePoint + 500) / 1000;
|
||||||
|
if (tseg1 < 1) tseg1 = 1;
|
||||||
|
tseg1 -= 1;
|
||||||
|
tseg1 = std::min(std::max(tseg1, tseg1Min), tseg1Max);
|
||||||
|
|
||||||
|
if (tqPerBit <= 1 + tseg1) continue;
|
||||||
|
quint32 tseg2 = tqPerBit - 1 - tseg1;
|
||||||
|
tseg2 = std::min(std::max(tseg2, tseg2Min), tseg2Max);
|
||||||
|
|
||||||
|
/* the clamp above may have moved tseg2, give the slack back to tseg1 */
|
||||||
|
if (tqPerBit <= 1 + tseg2) continue;
|
||||||
|
tseg1 = tqPerBit - 1 - tseg2;
|
||||||
|
if (tseg1 < tseg1Min || tseg1 > tseg1Max) continue;
|
||||||
|
|
||||||
|
const quint32 actualBitrate = pCaps.fclk_can / (brp * tqPerBit);
|
||||||
|
const quint32 actualSp = ((1 + tseg1) * 1000) / tqPerBit;
|
||||||
|
|
||||||
|
const quint64 bitrateErr = (actualBitrate > pBitrate) ? (actualBitrate - pBitrate)
|
||||||
|
: (pBitrate - actualBitrate);
|
||||||
|
const quint64 spErr = (actualSp > pSamplePoint) ? (actualSp - pSamplePoint)
|
||||||
|
: (pSamplePoint - actualSp);
|
||||||
|
/* bitrate accuracy dominates, the sample point only breaks ties */
|
||||||
|
const quint64 score = bitrateErr * 1000 + spErr;
|
||||||
|
|
||||||
|
if (!found || score < bestScore)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
bestScore = score;
|
||||||
|
|
||||||
|
pTiming.prop_seg = 1;
|
||||||
|
pTiming.phase_seg1 = tseg1 - 1;
|
||||||
|
pTiming.phase_seg2 = tseg2;
|
||||||
|
pTiming.sjw = 1;
|
||||||
|
pTiming.brp = brp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
/* device enumeration */
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Strip the USB interface number out of a Windows device path
|
||||||
|
*
|
||||||
|
* Composite gs_usb devices expose every CAN channel as its own Windows device path
|
||||||
|
* containing "&mi_XX". They all reach the same physical device and the same USB endpoints,
|
||||||
|
* so they have to collapse into one entry. Ported from cangaroo's CandleApiDriver.
|
||||||
|
*/
|
||||||
|
static std::wstring gsusbBaseDevicePath(const std::wstring& pPath)
|
||||||
|
{
|
||||||
|
/* Windows reports paths in lower case already, normalise anyway so the key is stable */
|
||||||
|
std::wstring result = pPath;
|
||||||
|
std::transform(result.begin(), result.end(), result.begin(), ::towlower);
|
||||||
|
|
||||||
|
/* &mi_NN shows up in both the hardware ID and the instance ID part of the path,
|
||||||
|
* so one pass is not enough */
|
||||||
|
const std::wstring miTag = L"&mi_";
|
||||||
|
auto pos = result.find(miTag);
|
||||||
|
while (pos != std::wstring::npos)
|
||||||
|
{
|
||||||
|
auto end = pos + miTag.size();
|
||||||
|
while (end < result.size() && iswxdigit(result[end])) ++end;
|
||||||
|
result.erase(pos, end - pos);
|
||||||
|
pos = result.find(miTag, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* devices without a USB serial number get a location based instance ID where the
|
||||||
|
* interface number is the last four hex digits before the interface GUID */
|
||||||
|
const auto guidPos = result.rfind(L"#{");
|
||||||
|
if (guidPos != std::wstring::npos && guidPos >= 5 && result[guidPos - 5] == L'&')
|
||||||
|
{
|
||||||
|
bool allHex = true;
|
||||||
|
for (std::size_t k = guidPos - 4; k < guidPos; ++k)
|
||||||
|
{
|
||||||
|
if (!iswxdigit(result[k])) { allHex = false; break; }
|
||||||
|
}
|
||||||
|
if (allHex) result.erase(guidPos - 5, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Derive a readable product name from the VID/PID in the device path
|
||||||
|
*/
|
||||||
|
static QString gsusbProductName(const std::wstring& pPath)
|
||||||
|
{
|
||||||
|
std::wstring lower = pPath;
|
||||||
|
std::transform(lower.begin(), lower.end(), lower.begin(), ::towlower);
|
||||||
|
|
||||||
|
auto extract = [&](const wchar_t* pTag) -> quint16 {
|
||||||
|
auto pos = lower.find(pTag);
|
||||||
|
if (pos == std::wstring::npos) return 0;
|
||||||
|
return static_cast<quint16>(wcstoul(lower.c_str() + pos + wcslen(pTag), nullptr, 16));
|
||||||
|
};
|
||||||
|
|
||||||
|
const quint16 vid = extract(L"vid_");
|
||||||
|
const quint16 pid = extract(L"pid_");
|
||||||
|
|
||||||
|
if (vid == 0x1D50 && pid == 0x606F) return QStringLiteral("candleLight");
|
||||||
|
if (vid == 0x1209 && pid == 0x2323) return QStringLiteral("CANable");
|
||||||
|
if (vid == 0x1209 && pid == 0xCA01) return QStringLiteral("CANnectivity");
|
||||||
|
if (vid == 0x1CD2 && pid == 0x606F) return QStringLiteral("CANable");
|
||||||
|
if (vid == 0x16D0 && pid == 0x117E) return QStringLiteral("cantact");
|
||||||
|
|
||||||
|
return QStringLiteral("gs_usb");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Channel counts seen in earlier scans, keyed by port name. A device that one of our own
|
||||||
|
* connections currently holds must not be probed again, so remember what it reported while
|
||||||
|
* it was still free.
|
||||||
|
*
|
||||||
|
* The probe matters: candle_dev_open() opens the device with FILE_SHARE_READ | FILE_SHARE_WRITE
|
||||||
|
* and immediately queues read URBs, so a second open on a device that is in use would consume
|
||||||
|
* frames the live connection is waiting for. gGSUSBOpenPaths tracks which base device paths are
|
||||||
|
* taken so enumerateDevices() can skip them. */
|
||||||
|
static QMutex gGSUSBCacheMutex;
|
||||||
|
static QHash<QString, GSUSBDeviceInfo> gGSUSBCache;
|
||||||
|
static QSet<QString> gGSUSBOpenPaths;
|
||||||
|
|
||||||
|
QList<GSUSBDeviceInfo> GSUSBConnection::enumerateDevices()
|
||||||
|
{
|
||||||
|
QList<GSUSBDeviceInfo> devices;
|
||||||
|
|
||||||
|
candle_list_handle list;
|
||||||
|
if (!candle_list_scan(&list)) return devices;
|
||||||
|
|
||||||
|
uint8_t numDevices = 0;
|
||||||
|
if (!candle_list_length(list, &numDevices))
|
||||||
|
{
|
||||||
|
candle_list_free(list);
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSet<QString> seenPaths;
|
||||||
|
QHash<QString, int> nameCounts;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < numDevices; i++)
|
||||||
|
{
|
||||||
|
candle_handle dev;
|
||||||
|
if (!candle_dev_get(list, i, &dev)) continue;
|
||||||
|
|
||||||
|
const std::wstring devPath(candle_dev_get_path(dev));
|
||||||
|
const QString baseKey = QString::fromWCharArray(gsusbBaseDevicePath(devPath).c_str());
|
||||||
|
|
||||||
|
/* additional USB interfaces of a device we already listed */
|
||||||
|
if (seenPaths.contains(baseKey))
|
||||||
|
{
|
||||||
|
candle_dev_free(dev);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenPaths.insert(baseKey);
|
||||||
|
|
||||||
|
GSUSBDeviceInfo info;
|
||||||
|
info.path = QString::fromWCharArray(devPath.c_str());
|
||||||
|
|
||||||
|
const QString product = gsusbProductName(devPath);
|
||||||
|
info.name = product % "#" % QString::number(nameCounts[product]++);
|
||||||
|
|
||||||
|
bool inUse;
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&gGSUSBCacheMutex);
|
||||||
|
inUse = gGSUSBOpenPaths.contains(baseKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the channel count only becomes readable once the device is open */
|
||||||
|
if (!inUse && candle_dev_open(dev))
|
||||||
|
{
|
||||||
|
uint8_t numChannels = 0;
|
||||||
|
if (candle_channel_count(dev, &numChannels) && numChannels > 0)
|
||||||
|
info.numChannels = std::min<int>(numChannels, GSUSB_MAX_CHANNELS);
|
||||||
|
candle_dev_close(dev);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* held by one of our connections, or the open failed, so reuse the earlier answer */
|
||||||
|
QMutexLocker locker(&gGSUSBCacheMutex);
|
||||||
|
if (gGSUSBCache.contains(info.name)) info.numChannels = gGSUSBCache[info.name].numChannels;
|
||||||
|
}
|
||||||
|
candle_dev_free(dev);
|
||||||
|
|
||||||
|
devices.append(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_list_free(list);
|
||||||
|
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&gGSUSBCacheMutex);
|
||||||
|
foreach (const GSUSBDeviceInfo& info, devices) gGSUSBCache[info.name] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GSUSBConnection::channelCountForPort(const QString& portName)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&gGSUSBCacheMutex);
|
||||||
|
if (gGSUSBCache.contains(portName)) return gGSUSBCache[portName].numChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (const GSUSBDeviceInfo& info, enumerateDevices())
|
||||||
|
{
|
||||||
|
if (info.name == portName) return info.numChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* device not plugged in right now, assume a single channel so the connection can still be
|
||||||
|
* created and shown as disconnected */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
/* connection */
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
GSUSBConnection::GSUSBConnection(QString portName, int busSpeed, bool canFd, int dataRate) :
|
||||||
|
CANConnection(portName, "GS_USB", CANCon::GS_USB, 0, busSpeed, canFd, dataRate,
|
||||||
|
channelCountForPort(portName), 4000, true),
|
||||||
|
mDevice(nullptr),
|
||||||
|
mDeviceChannels(0),
|
||||||
|
mReaderRunning(false),
|
||||||
|
mRxNotifyPending(false),
|
||||||
|
mHostOffsetStartUs(0),
|
||||||
|
mDeviceTicksStartUs(0),
|
||||||
|
mDeviceTsHigh(0),
|
||||||
|
mPrevDeviceTs(0),
|
||||||
|
mDeviceTsValid(false)
|
||||||
|
{
|
||||||
|
sendDebug("GSUSBConnection()");
|
||||||
|
|
||||||
|
mChannelRunning.fill(false, getNumBuses());
|
||||||
|
mFdEnabled.fill(false, getNumBuses());
|
||||||
|
|
||||||
|
/* The connection window only ever persists the settings of bus 0 and refuses to show a bus
|
||||||
|
* that was never configured, so give every channel a usable default up front. */
|
||||||
|
for (int i = 0; i < getNumBuses(); i++)
|
||||||
|
{
|
||||||
|
CANBus bus;
|
||||||
|
bus.setSpeed(busSpeed > 0 ? busSpeed : 500000);
|
||||||
|
bus.setDataRate(dataRate > 0 ? dataRate : 2000000);
|
||||||
|
bus.setCanFD(canFd);
|
||||||
|
bus.setListenOnly(false);
|
||||||
|
bus.setSingleWire(false);
|
||||||
|
bus.setActive(true);
|
||||||
|
setBusConfig(i, bus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GSUSBConnection::~GSUSBConnection()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
sendDebug("~GSUSBConnection()");
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::sendDebug(const QString& pDebugText)
|
||||||
|
{
|
||||||
|
qDebug() << pDebugText;
|
||||||
|
emit debugOutput(pDebugText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::piStarted()
|
||||||
|
{
|
||||||
|
if (!openDevice())
|
||||||
|
{
|
||||||
|
setStatus(CANCon::NOT_CONNECTED);
|
||||||
|
CANConStatus stats;
|
||||||
|
stats.conStatus = getStatus();
|
||||||
|
stats.numHardwareBuses = getNumBuses();
|
||||||
|
emit status(stats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < getNumBuses(); i++)
|
||||||
|
{
|
||||||
|
CANBus bus;
|
||||||
|
if (!getBusConfig(i, bus)) continue;
|
||||||
|
configureChannel(i, bus);
|
||||||
|
}
|
||||||
|
|
||||||
|
startReader();
|
||||||
|
|
||||||
|
setStatus(CANCon::CONNECTED);
|
||||||
|
CANConStatus stats;
|
||||||
|
stats.conStatus = getStatus();
|
||||||
|
stats.numHardwareBuses = getNumBuses();
|
||||||
|
emit status(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::piStop()
|
||||||
|
{
|
||||||
|
stopReader();
|
||||||
|
|
||||||
|
for (int i = 0; i < getNumBuses(); i++) stopChannel(i);
|
||||||
|
|
||||||
|
closeDevice();
|
||||||
|
|
||||||
|
setStatus(CANCon::NOT_CONNECTED);
|
||||||
|
CANConStatus stats;
|
||||||
|
stats.conStatus = getStatus();
|
||||||
|
stats.numHardwareBuses = getNumBuses();
|
||||||
|
emit status(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::piSuspend(bool pSuspend)
|
||||||
|
{
|
||||||
|
setCapSuspended(pSuspend);
|
||||||
|
|
||||||
|
if (isCapSuspended())
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&mRxMutex);
|
||||||
|
mRxFrames.clear();
|
||||||
|
getQueue().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GSUSBConnection::piGetBusSettings(int pBusIdx, CANBus& pBus)
|
||||||
|
{
|
||||||
|
return getBusConfig(pBusIdx, pBus);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::piSetBusSettings(int pBusIdx, CANBus bus)
|
||||||
|
{
|
||||||
|
if (pBusIdx < 0 || pBusIdx >= getNumBuses()) return;
|
||||||
|
|
||||||
|
setBusConfig(pBusIdx, bus);
|
||||||
|
|
||||||
|
if (!mDevice) return;
|
||||||
|
|
||||||
|
/* gs_usb only accepts a new bit timing while the channel is stopped */
|
||||||
|
stopChannel(pBusIdx);
|
||||||
|
configureChannel(pBusIdx, bus);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GSUSBConnection::piSendFrame(const CANFrame& frame)
|
||||||
|
{
|
||||||
|
if (!mDevice) return false;
|
||||||
|
|
||||||
|
const int bus = frame.bus;
|
||||||
|
if (bus < 0 || bus >= getNumBuses()) return false;
|
||||||
|
if (!mChannelRunning[bus]) return false;
|
||||||
|
|
||||||
|
/* error frames are a reporting mechanism, they cannot be put on the wire */
|
||||||
|
if (frame.frameId() & 0x20000000) return true;
|
||||||
|
|
||||||
|
const QByteArray payload = frame.payload();
|
||||||
|
const bool sendAsFd = mFdEnabled[bus] && (frame.hasFlexibleDataRateFormat() || payload.length() > 8);
|
||||||
|
|
||||||
|
QMutexLocker locker(&mTxMutex);
|
||||||
|
bool ok = false;
|
||||||
|
|
||||||
|
if (sendAsFd)
|
||||||
|
{
|
||||||
|
candle_fd_frame_t out;
|
||||||
|
memset(&out, 0, sizeof(out));
|
||||||
|
|
||||||
|
out.can_id = frame.frameId();
|
||||||
|
if (frame.hasExtendedFrameFormat()) out.can_id |= CANDLE_ID_EXTENDED;
|
||||||
|
|
||||||
|
out.flags = CANDLE_FRAME_FLAG_FD;
|
||||||
|
if (frame.hasBitrateSwitch()) out.flags |= CANDLE_FRAME_FLAG_BRS;
|
||||||
|
|
||||||
|
const int len = std::min<int>(payload.length(), 64);
|
||||||
|
out.can_dlc = candle_len_to_dlc(static_cast<uint8_t>(len));
|
||||||
|
/* a DLC always maps to a fixed length, pad the rest with zeroes */
|
||||||
|
for (int i = 0; i < len; i++) out.data[i] = static_cast<uint8_t>(payload[i]);
|
||||||
|
|
||||||
|
ok = candle_fd_frame_send(mDevice, static_cast<uint8_t>(bus), &out);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
candle_frame_t out;
|
||||||
|
memset(&out, 0, sizeof(out));
|
||||||
|
|
||||||
|
out.can_id = frame.frameId();
|
||||||
|
if (frame.hasExtendedFrameFormat()) out.can_id |= CANDLE_ID_EXTENDED;
|
||||||
|
if (frame.frameType() == QCanBusFrame::RemoteRequestFrame) out.can_id |= CANDLE_ID_RTR;
|
||||||
|
|
||||||
|
const int len = std::min<int>(payload.length(), 8);
|
||||||
|
out.can_dlc = static_cast<uint8_t>(len);
|
||||||
|
for (int i = 0; i < len; i++) out.data[i] = static_cast<uint8_t>(payload[i]);
|
||||||
|
|
||||||
|
ok = candle_frame_send(mDevice, static_cast<uint8_t>(bus), &out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok) sendDebug("GS_USB: frame send failed with error " % QString::number(static_cast<int>(candle_dev_last_error(mDevice))));
|
||||||
|
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
/* device handling */
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
bool GSUSBConnection::openDevice()
|
||||||
|
{
|
||||||
|
if (mDevice) return true;
|
||||||
|
|
||||||
|
candle_list_handle list;
|
||||||
|
if (!candle_list_scan(&list))
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: could not scan the USB bus");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t numDevices = 0;
|
||||||
|
if (!candle_list_length(list, &numDevices))
|
||||||
|
{
|
||||||
|
candle_list_free(list);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resolve the stored port name back to a device. The name carries the index among all
|
||||||
|
* devices of the same product, so rebuild the same numbering the scan produced. */
|
||||||
|
QSet<QString> seenPaths;
|
||||||
|
QHash<QString, int> nameCounts;
|
||||||
|
candle_handle match = nullptr;
|
||||||
|
std::wstring matchPath;
|
||||||
|
QString matchBaseKey;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < numDevices; i++)
|
||||||
|
{
|
||||||
|
candle_handle dev;
|
||||||
|
if (!candle_dev_get(list, i, &dev)) continue;
|
||||||
|
|
||||||
|
const std::wstring devPath(candle_dev_get_path(dev));
|
||||||
|
const QString baseKey = QString::fromWCharArray(gsusbBaseDevicePath(devPath).c_str());
|
||||||
|
if (seenPaths.contains(baseKey))
|
||||||
|
{
|
||||||
|
candle_dev_free(dev);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenPaths.insert(baseKey);
|
||||||
|
|
||||||
|
const QString product = gsusbProductName(devPath);
|
||||||
|
const QString name = product % "#" % QString::number(nameCounts[product]++);
|
||||||
|
|
||||||
|
if (name == getPort() && !match)
|
||||||
|
{
|
||||||
|
match = dev;
|
||||||
|
matchPath = devPath;
|
||||||
|
matchBaseKey = baseKey;
|
||||||
|
}
|
||||||
|
else candle_dev_free(dev);
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_list_free(list);
|
||||||
|
|
||||||
|
if (!match)
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: device " % getPort() % " not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candle_dev_open(match))
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: could not open " % getPort() % ", error " % QString::number(static_cast<int>(candle_dev_last_error(match))));
|
||||||
|
candle_dev_free(match);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The number of buses was fixed when the connection was constructed. If the device now
|
||||||
|
* reports fewer channels (a different device took over the name) only drive what exists. */
|
||||||
|
uint8_t numChannels = 0;
|
||||||
|
mDeviceChannels = getNumBuses();
|
||||||
|
if (candle_channel_count(match, &numChannels) && numChannels > 0)
|
||||||
|
mDeviceChannels = std::min<int>(numChannels, getNumBuses());
|
||||||
|
|
||||||
|
if (mDeviceChannels < getNumBuses())
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: device reports " % QString::number(numChannels) %
|
||||||
|
" channels but the connection was created with " % QString::number(getNumBuses()));
|
||||||
|
}
|
||||||
|
|
||||||
|
mDevice = match;
|
||||||
|
mDevicePath = QString::fromWCharArray(matchPath.c_str());
|
||||||
|
mDeviceBaseKey = matchBaseKey;
|
||||||
|
|
||||||
|
{
|
||||||
|
/* keep device scans from opening this device behind our back */
|
||||||
|
QMutexLocker locker(&gGSUSBCacheMutex);
|
||||||
|
gGSUSBOpenPaths.insert(mDeviceBaseKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anchor the device clock to the host clock so frame timestamps line up with the rest of
|
||||||
|
* SavvyCAN. Both are microseconds since the epoch after this. */
|
||||||
|
const quint64 hostNowUs = static_cast<quint64>(QDateTime::currentMSecsSinceEpoch()) * 1000ull;
|
||||||
|
uint32_t deviceTicks = 0;
|
||||||
|
mHostOffsetStartUs = hostNowUs;
|
||||||
|
mDeviceTicksStartUs = 0;
|
||||||
|
mDeviceTsHigh = 0;
|
||||||
|
mPrevDeviceTs = 0;
|
||||||
|
mDeviceTsValid = false;
|
||||||
|
|
||||||
|
if (candle_dev_get_timestamp_us(mDevice, &deviceTicks))
|
||||||
|
{
|
||||||
|
mDeviceTicksStartUs = deviceTicks;
|
||||||
|
mDeviceTsValid = true;
|
||||||
|
}
|
||||||
|
else sendDebug("GS_USB: no hardware timestamps, falling back to the host clock");
|
||||||
|
|
||||||
|
sendDebug("GS_USB: opened " % getPort() % " with " % QString::number(getNumBuses()) % " channel(s)");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::closeDevice()
|
||||||
|
{
|
||||||
|
if (!mDevice) return;
|
||||||
|
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&gGSUSBCacheMutex);
|
||||||
|
gGSUSBOpenPaths.remove(mDeviceBaseKey);
|
||||||
|
}
|
||||||
|
mDeviceBaseKey.clear();
|
||||||
|
|
||||||
|
candle_dev_close(mDevice);
|
||||||
|
candle_dev_free(mDevice);
|
||||||
|
mDevice = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GSUSBConnection::configureChannel(int pBusIdx, const CANBus& pBus)
|
||||||
|
{
|
||||||
|
if (!mDevice) return false;
|
||||||
|
if (pBusIdx < 0 || pBusIdx >= getNumBuses()) return false;
|
||||||
|
|
||||||
|
mFdEnabled[pBusIdx] = false;
|
||||||
|
|
||||||
|
if (pBusIdx >= mDeviceChannels) return false;
|
||||||
|
|
||||||
|
/* bits 3, 5 and 6 tell the connection window that the enabled, listen only and speed
|
||||||
|
* values in this update are meaningful */
|
||||||
|
const int busStatValid = 0x08 | 0x20 | 0x40;
|
||||||
|
|
||||||
|
if (!pBus.isActive())
|
||||||
|
{
|
||||||
|
emit busStatus(pBusIdx, pBus.getSpeed(), busStatValid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t channel = static_cast<uint8_t>(pBusIdx);
|
||||||
|
|
||||||
|
candle_capability_t caps;
|
||||||
|
if (!candle_channel_get_capabilities(mDevice, channel, &caps))
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: could not read capabilities of channel " % QString::number(pBusIdx));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
candle_bittiming_t timing;
|
||||||
|
if (!lookupBitTiming(GSUSB_NOMINAL_TIMINGS,
|
||||||
|
sizeof(GSUSB_NOMINAL_TIMINGS) / sizeof(GSUSB_NOMINAL_TIMINGS[0]),
|
||||||
|
caps.fclk_can, static_cast<quint32>(pBus.getSpeed()), timing)
|
||||||
|
&& !calcBitTiming(caps, static_cast<quint32>(pBus.getSpeed()), GSUSB_NOMINAL_SAMPLE_POINT, false, timing))
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: no bit timing for " % QString::number(pBus.getSpeed()) %
|
||||||
|
" bit/s at a " % QString::number(caps.fclk_can) % " Hz clock");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candle_channel_set_timing(mDevice, channel, &timing))
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: setting the bit timing of channel " % QString::number(pBusIdx) % " failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t flags = 0;
|
||||||
|
if (pBus.isListenOnly()) flags |= CANDLE_MODE_LISTEN_ONLY;
|
||||||
|
if (caps.feature & CANDLE_FEATURE_HW_TIMESTAMP) flags |= CANDLE_MODE_HW_TIMESTAMP;
|
||||||
|
|
||||||
|
if (pBus.isCanFD())
|
||||||
|
{
|
||||||
|
if (!(caps.feature & CANDLE_FEATURE_FD))
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: channel " % QString::number(pBusIdx) % " does not support CAN FD");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const quint32 dataRate = pBus.getDataRate() > 0 ? static_cast<quint32>(pBus.getDataRate())
|
||||||
|
: 2000000u;
|
||||||
|
candle_bittiming_t dataTiming;
|
||||||
|
if (lookupBitTiming(GSUSB_DATA_TIMINGS,
|
||||||
|
sizeof(GSUSB_DATA_TIMINGS) / sizeof(GSUSB_DATA_TIMINGS[0]),
|
||||||
|
caps.fclk_can, dataRate, dataTiming)
|
||||||
|
|| calcBitTiming(caps, dataRate, GSUSB_DATA_SAMPLE_POINT, true, dataTiming))
|
||||||
|
{
|
||||||
|
if (candle_channel_set_data_timing(mDevice, channel, &dataTiming))
|
||||||
|
{
|
||||||
|
flags |= CANDLE_MODE_FD;
|
||||||
|
mFdEnabled[pBusIdx] = true;
|
||||||
|
}
|
||||||
|
else sendDebug("GS_USB: setting the data bit timing failed, staying on classic CAN");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: no data bit timing for " % QString::number(dataRate) %
|
||||||
|
" bit/s, staying on classic CAN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candle_channel_start(mDevice, channel, flags))
|
||||||
|
{
|
||||||
|
sendDebug("GS_USB: starting channel " % QString::number(pBusIdx) % " failed, error " %
|
||||||
|
QString::number(static_cast<int>(candle_dev_last_error(mDevice))));
|
||||||
|
mFdEnabled[pBusIdx] = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mChannelRunning[pBusIdx] = true;
|
||||||
|
|
||||||
|
int busStat = busStatValid | 1;
|
||||||
|
if (pBus.isListenOnly()) busStat |= 4;
|
||||||
|
emit busStatus(pBusIdx, pBus.getSpeed(), busStat);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::stopChannel(int pBusIdx)
|
||||||
|
{
|
||||||
|
if (!mDevice) return;
|
||||||
|
if (pBusIdx < 0 || pBusIdx >= getNumBuses()) return;
|
||||||
|
if (!mChannelRunning[pBusIdx]) return;
|
||||||
|
|
||||||
|
|
||||||
|
candle_channel_stop(mDevice, static_cast<uint8_t>(pBusIdx));
|
||||||
|
mChannelRunning[pBusIdx] = false;
|
||||||
|
mFdEnabled[pBusIdx] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
/* receive path */
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
void GSUSBConnection::startReader()
|
||||||
|
{
|
||||||
|
if (mReaderRunning.load()) return;
|
||||||
|
|
||||||
|
/* a notification that was posted but never delivered would otherwise block every
|
||||||
|
* later one, so clear it before the thread starts producing again */
|
||||||
|
mRxNotifyPending.store(false);
|
||||||
|
mReaderRunning.store(true);
|
||||||
|
mReaderThread = std::thread(&GSUSBConnection::readerLoop, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::stopReader()
|
||||||
|
{
|
||||||
|
mReaderRunning.store(false);
|
||||||
|
if (mReaderThread.joinable()) mReaderThread.join();
|
||||||
|
mRxNotifyPending.store(false);
|
||||||
|
|
||||||
|
QMutexLocker locker(&mRxMutex);
|
||||||
|
mRxFrames.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::readerLoop()
|
||||||
|
{
|
||||||
|
while (mReaderRunning.load())
|
||||||
|
{
|
||||||
|
candle_fd_frame_t frame;
|
||||||
|
if (!candle_fd_frame_read(mDevice, &frame, GSUSB_READ_TIMEOUT_MS)) continue;
|
||||||
|
|
||||||
|
const candle_frametype_t frameType = candle_fd_frame_type(&frame);
|
||||||
|
if (frameType != CANDLE_FRAMETYPE_RECEIVE && frameType != CANDLE_FRAMETYPE_ERROR) continue;
|
||||||
|
if (static_cast<int>(frame.channel) >= getNumBuses()) continue;
|
||||||
|
|
||||||
|
GSUSBRxFrame rx;
|
||||||
|
rx.frame = frame;
|
||||||
|
rx.timestampUs = 0;
|
||||||
|
rx.timestampValid = deviceTimestampToHostUs(candle_fd_frame_timestamp_us(&frame), rx.timestampUs);
|
||||||
|
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&mRxMutex);
|
||||||
|
mRxFrames.append(rx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hand over to the connection thread. Only post a new event when the previous one has
|
||||||
|
* not been processed yet, otherwise a busy bus floods the event loop. */
|
||||||
|
if (!mRxNotifyPending.exchange(true))
|
||||||
|
QMetaObject::invokeMethod(this, "processRxFrames", Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GSUSBConnection::deviceTimestampToHostUs(quint32 pRawTimestampUs, quint64& pHostTimestampUs)
|
||||||
|
{
|
||||||
|
/* the firmware counter is 32 bit microseconds, so it wraps roughly every 71 minutes */
|
||||||
|
static const quint32 wrapThreshold = 0x80000000u;
|
||||||
|
|
||||||
|
if (!mDeviceTsValid) return false;
|
||||||
|
|
||||||
|
if (mPrevDeviceTs != 0 && pRawTimestampUs < mPrevDeviceTs
|
||||||
|
&& (mPrevDeviceTs - pRawTimestampUs) > wrapThreshold)
|
||||||
|
{
|
||||||
|
mDeviceTsHigh += (1ull << 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 absTimestampUs = static_cast<quint64>(pRawTimestampUs) + mDeviceTsHigh;
|
||||||
|
|
||||||
|
/* the first frame after opening may predate the epoch sample, either because it was
|
||||||
|
* buffered in the device or because the counter wrapped in between */
|
||||||
|
if (mPrevDeviceTs == 0 && absTimestampUs < mDeviceTicksStartUs)
|
||||||
|
{
|
||||||
|
if ((mDeviceTicksStartUs - absTimestampUs) > wrapThreshold)
|
||||||
|
{
|
||||||
|
mDeviceTsHigh += (1ull << 32);
|
||||||
|
absTimestampUs += (1ull << 32);
|
||||||
|
}
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absTimestampUs < mDeviceTicksStartUs) return false;
|
||||||
|
|
||||||
|
mPrevDeviceTs = pRawTimestampUs;
|
||||||
|
pHostTimestampUs = mHostOffsetStartUs + (absTimestampUs - mDeviceTicksStartUs);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GSUSBConnection::processRxFrames()
|
||||||
|
{
|
||||||
|
mRxNotifyPending.store(false);
|
||||||
|
|
||||||
|
QVector<GSUSBRxFrame> frames;
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&mRxMutex);
|
||||||
|
frames.swap(mRxFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCapSuspended()) return;
|
||||||
|
|
||||||
|
foreach (const GSUSBRxFrame& rx, frames)
|
||||||
|
{
|
||||||
|
candle_fd_frame_t frame = rx.frame;
|
||||||
|
|
||||||
|
CANFrame buildFrame;
|
||||||
|
buildFrame.bus = frame.channel;
|
||||||
|
buildFrame.isReceived = true;
|
||||||
|
buildFrame.setFrameId(candle_fd_frame_id(&frame));
|
||||||
|
buildFrame.setExtendedFrameFormat(candle_fd_frame_is_extended_id(&frame));
|
||||||
|
|
||||||
|
if (candle_fd_frame_type(&frame) == CANDLE_FRAMETYPE_ERROR)
|
||||||
|
buildFrame.setFrameType(QCanBusFrame::ErrorFrame);
|
||||||
|
else if (candle_fd_frame_is_rtr(&frame))
|
||||||
|
buildFrame.setFrameType(QCanBusFrame::RemoteRequestFrame);
|
||||||
|
else
|
||||||
|
buildFrame.setFrameType(QCanBusFrame::DataFrame);
|
||||||
|
|
||||||
|
const bool isFd = candle_fd_frame_is_fd(&frame);
|
||||||
|
buildFrame.setFlexibleDataRateFormat(isFd);
|
||||||
|
if (isFd) buildFrame.setBitrateSwitch(candle_fd_frame_is_brs(&frame));
|
||||||
|
|
||||||
|
const uint8_t rawDlc = candle_fd_frame_dlc(&frame);
|
||||||
|
const int len = isFd ? candle_dlc_to_len(rawDlc) : std::min<int>(rawDlc, 8);
|
||||||
|
const uint8_t* data = candle_fd_frame_data(&frame);
|
||||||
|
|
||||||
|
QByteArray buildData;
|
||||||
|
buildData.resize(len);
|
||||||
|
for (int i = 0; i < len; i++) buildData[i] = static_cast<char>(data[i]);
|
||||||
|
buildFrame.setPayload(buildData);
|
||||||
|
|
||||||
|
const quint64 timestampUs = (useSystemTime || !rx.timestampValid)
|
||||||
|
? static_cast<quint64>(QDateTime::currentMSecsSinceEpoch()) * 1000ull
|
||||||
|
: rx.timestampUs;
|
||||||
|
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, static_cast<qint64>(timestampUs)));
|
||||||
|
|
||||||
|
CANFrame* frame_p = getQueue().get();
|
||||||
|
if (!frame_p)
|
||||||
|
{
|
||||||
|
qDebug() << "GS_USB: can't get a frame, ERROR";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
*frame_p = buildFrame;
|
||||||
|
checkTargettedFrame(buildFrame);
|
||||||
|
getQueue().queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // Q_OS_WIN
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#ifndef GS_USB_H
|
||||||
|
#define GS_USB_H
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "canconnection.h"
|
||||||
|
#include "candle_api/candle.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A gs_usb device as reported by a USB bus scan
|
||||||
|
*/
|
||||||
|
struct GSUSBDeviceInfo
|
||||||
|
{
|
||||||
|
QString name; //!< port name shown in the UI and stored in settings, e.g. "CANable#0"
|
||||||
|
QString path; //!< Windows device interface path, unique per physical device
|
||||||
|
int numChannels; //!< number of CAN channels the device reports
|
||||||
|
|
||||||
|
GSUSBDeviceInfo() : numChannels(1) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A single RX frame handed from the USB reader thread to the connection thread
|
||||||
|
*/
|
||||||
|
struct GSUSBRxFrame
|
||||||
|
{
|
||||||
|
candle_fd_frame_t frame;
|
||||||
|
quint64 timestampUs;
|
||||||
|
bool timestampValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Connection to a gs_usb class device (candleLight, CANable, CANnectivity, cantact, ...)
|
||||||
|
*
|
||||||
|
* Windows only. On Linux and macOS these devices are handled by the kernel gs_usb driver and
|
||||||
|
* show up as SocketCAN interfaces, so no user space driver is needed there.
|
||||||
|
*
|
||||||
|
* One connection owns one physical device. All CAN channels of that device are exposed as
|
||||||
|
* SavvyCAN buses. gs_usb multiplexes every channel over a single USB bulk IN endpoint, so a
|
||||||
|
* single reader thread reads the endpoint and fans frames out by their channel number.
|
||||||
|
*/
|
||||||
|
class GSUSBConnection : public CANConnection
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
GSUSBConnection(QString portName, int busSpeed, bool canFd, int dataRate);
|
||||||
|
virtual ~GSUSBConnection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Scan the USB bus for gs_usb devices
|
||||||
|
* @return one entry per physical device (not per channel)
|
||||||
|
* @note Devices that are currently open by another connection cannot be probed for their
|
||||||
|
* channel count, so a previously scanned value is reused for those.
|
||||||
|
*/
|
||||||
|
static QList<GSUSBDeviceInfo> enumerateDevices();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void piStarted();
|
||||||
|
virtual void piStop();
|
||||||
|
virtual void piSetBusSettings(int pBusIdx, CANBus pBus);
|
||||||
|
virtual bool piGetBusSettings(int pBusIdx, CANBus& pBus);
|
||||||
|
virtual void piSuspend(bool pSuspend);
|
||||||
|
virtual bool piSendFrame(const CANFrame&);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
/**
|
||||||
|
* @brief Move everything the reader thread collected into the connection queue.
|
||||||
|
* @note Runs in the connection thread.
|
||||||
|
*/
|
||||||
|
void processRxFrames();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/* number of buses to hand to the CANConnection constructor, needs the device before we have one */
|
||||||
|
static int channelCountForPort(const QString& portName);
|
||||||
|
|
||||||
|
bool openDevice();
|
||||||
|
void closeDevice();
|
||||||
|
bool configureChannel(int pBusIdx, const CANBus& pBus);
|
||||||
|
void stopChannel(int pBusIdx);
|
||||||
|
void startReader();
|
||||||
|
void stopReader();
|
||||||
|
void readerLoop();
|
||||||
|
bool deviceTimestampToHostUs(quint32 pRawTimestampUs, quint64& pHostTimestampUs);
|
||||||
|
void sendDebug(const QString& pDebugText);
|
||||||
|
|
||||||
|
candle_handle mDevice;
|
||||||
|
QString mDevicePath;
|
||||||
|
QString mDeviceBaseKey; //!< device path with the USB interface number stripped
|
||||||
|
int mDeviceChannels; //!< channels the device actually reports, <= getNumBuses()
|
||||||
|
|
||||||
|
std::thread mReaderThread;
|
||||||
|
std::atomic<bool> mReaderRunning;
|
||||||
|
std::atomic<bool> mRxNotifyPending;
|
||||||
|
|
||||||
|
QMutex mRxMutex;
|
||||||
|
QVector<GSUSBRxFrame> mRxFrames;
|
||||||
|
|
||||||
|
QMutex mTxMutex;
|
||||||
|
|
||||||
|
QVector<bool> mChannelRunning;
|
||||||
|
QVector<bool> mFdEnabled;
|
||||||
|
|
||||||
|
/* device clock to host clock mapping, only written by the reader thread once it is running */
|
||||||
|
quint64 mHostOffsetStartUs;
|
||||||
|
quint64 mDeviceTicksStartUs;
|
||||||
|
quint64 mDeviceTsHigh;
|
||||||
|
quint32 mPrevDeviceTs;
|
||||||
|
bool mDeviceTsValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // Q_OS_WIN
|
||||||
|
|
||||||
|
#endif // GS_USB_H
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
#include "newconnectiondialog.h"
|
#include "newconnectiondialog.h"
|
||||||
#include "ui_newconnectiondialog.h"
|
#include "ui_newconnectiondialog.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
#include "gs_usb.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
NewConnectionDialog::NewConnectionDialog(QVector<QString>* gvretips, QVector<QString>* kayakhosts, QWidget *parent) :
|
NewConnectionDialog::NewConnectionDialog(QVector<QString>* gvretips, QVector<QString>* kayakhosts, QWidget *parent) :
|
||||||
QDialog(parent),
|
QDialog(parent),
|
||||||
ui(new Ui::NewConnectionDialog),
|
ui(new Ui::NewConnectionDialog),
|
||||||
@@ -30,6 +34,13 @@ NewConnectionDialog::NewConnectionDialog(QVector<QString>* gvretips, QVector<QSt
|
|||||||
connect(ui->rbLawicel, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged);
|
connect(ui->rbLawicel, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged);
|
||||||
connect(ui->rbCANserver, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged);
|
connect(ui->rbCANserver, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged);
|
||||||
connect(ui->rbCanlogserver, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged);
|
connect(ui->rbCanlogserver, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged);
|
||||||
|
connect(ui->rbGSUSB, &QAbstractButton::clicked, this, &NewConnectionDialog::handleConnTypeChanged);
|
||||||
|
|
||||||
|
#ifndef Q_OS_WIN
|
||||||
|
//on Linux and macOS the kernel gs_usb driver already exposes these devices as SocketCAN
|
||||||
|
ui->rbGSUSB->setEnabled(false);
|
||||||
|
ui->rbGSUSB->setToolTip("Connect gs_usb devices through SocketCAN on this platform");
|
||||||
|
#endif
|
||||||
|
|
||||||
connect(ui->cbDeviceType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &NewConnectionDialog::handleDeviceTypeChanged);
|
connect(ui->cbDeviceType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &NewConnectionDialog::handleDeviceTypeChanged);
|
||||||
connect(ui->btnOK, &QPushButton::clicked, this, &NewConnectionDialog::handleCreateButton);
|
connect(ui->btnOK, &QPushButton::clicked, this, &NewConnectionDialog::handleCreateButton);
|
||||||
@@ -70,6 +81,7 @@ void NewConnectionDialog::handleConnTypeChanged()
|
|||||||
if (ui->rbMQTT->isChecked()) selectMQTT();
|
if (ui->rbMQTT->isChecked()) selectMQTT();
|
||||||
if (ui->rbCANserver->isChecked()) selectCANserver();
|
if (ui->rbCANserver->isChecked()) selectCANserver();
|
||||||
if (ui->rbCanlogserver->isChecked()) selectCANlogserver();
|
if (ui->rbCanlogserver->isChecked()) selectCANlogserver();
|
||||||
|
if (ui->rbGSUSB->isChecked()) selectGSUSB();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewConnectionDialog::handleDeviceTypeChanged()
|
void NewConnectionDialog::handleDeviceTypeChanged()
|
||||||
@@ -270,6 +282,52 @@ void NewConnectionDialog::selectCANlogserver()
|
|||||||
ui->cbPort->clear();
|
ui->cbPort->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NewConnectionDialog::selectGSUSB()
|
||||||
|
{
|
||||||
|
ui->lPort->setText("gs_usb Device:");
|
||||||
|
|
||||||
|
ui->lblDeviceType->setHidden(true);
|
||||||
|
ui->cbDeviceType->setHidden(true);
|
||||||
|
|
||||||
|
ui->cbCANSpeed->setHidden(false);
|
||||||
|
ui->lblCANSpeed->setHidden(false);
|
||||||
|
ui->cbSerialSpeed->setHidden(true);
|
||||||
|
ui->lblSerialSpeed->setHidden(true);
|
||||||
|
ui->cbCanFd->setHidden(false);
|
||||||
|
ui->cbDataRate->setHidden(false);
|
||||||
|
ui->lblDataRate->setHidden(false);
|
||||||
|
|
||||||
|
ui->cbPort->clear();
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
foreach (const GSUSBDeviceInfo &dev, GSUSBConnection::enumerateDevices())
|
||||||
|
ui->cbPort->addItem(dev.name);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (ui->cbCANSpeed->count() == 0)
|
||||||
|
{
|
||||||
|
ui->cbCANSpeed->addItem("10000");
|
||||||
|
ui->cbCANSpeed->addItem("20000");
|
||||||
|
ui->cbCANSpeed->addItem("50000");
|
||||||
|
ui->cbCANSpeed->addItem("83333");
|
||||||
|
ui->cbCANSpeed->addItem("100000");
|
||||||
|
ui->cbCANSpeed->addItem("125000");
|
||||||
|
ui->cbCANSpeed->addItem("250000");
|
||||||
|
ui->cbCANSpeed->addItem("500000");
|
||||||
|
ui->cbCANSpeed->addItem("1000000");
|
||||||
|
}
|
||||||
|
if (ui->cbDataRate->count() == 0)
|
||||||
|
{
|
||||||
|
ui->cbDataRate->addItem("1000000");
|
||||||
|
ui->cbDataRate->addItem("2000000");
|
||||||
|
ui->cbDataRate->addItem("4000000");
|
||||||
|
ui->cbDataRate->addItem("5000000");
|
||||||
|
}
|
||||||
|
|
||||||
|
//default to the most common bus speed rather than the lowest one in the list
|
||||||
|
int idx = ui->cbCANSpeed->findText("500000");
|
||||||
|
if (idx > -1) ui->cbCANSpeed->setCurrentIndex(idx);
|
||||||
|
}
|
||||||
|
|
||||||
void NewConnectionDialog::setPortName(CANCon::type pType, QString pPortName, QString pDriver)
|
void NewConnectionDialog::setPortName(CANCon::type pType, QString pPortName, QString pDriver)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -299,6 +357,9 @@ void NewConnectionDialog::setPortName(CANCon::type pType, QString pPortName, QSt
|
|||||||
case CANCon::CANLOGSERVER:
|
case CANCon::CANLOGSERVER:
|
||||||
ui->rbCanlogserver->setChecked(true);
|
ui->rbCanlogserver->setChecked(true);
|
||||||
break;
|
break;
|
||||||
|
case CANCon::GS_USB:
|
||||||
|
ui->rbGSUSB->setChecked(true);
|
||||||
|
break;
|
||||||
default: {}
|
default: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +370,7 @@ void NewConnectionDialog::setPortName(CANCon::type pType, QString pPortName, QSt
|
|||||||
{
|
{
|
||||||
case CANCon::GVRET_SERIAL:
|
case CANCon::GVRET_SERIAL:
|
||||||
case CANCon::LAWICEL:
|
case CANCon::LAWICEL:
|
||||||
|
case CANCon::GS_USB:
|
||||||
{
|
{
|
||||||
int idx = ui->cbPort->findText(pPortName);
|
int idx = ui->cbPort->findText(pPortName);
|
||||||
if( idx<0 ) idx=0;
|
if( idx<0 ) idx=0;
|
||||||
@@ -360,6 +422,7 @@ QString NewConnectionDialog::getPortName()
|
|||||||
case CANCon::REMOTE:
|
case CANCon::REMOTE:
|
||||||
case CANCon::MQTT:
|
case CANCon::MQTT:
|
||||||
case CANCon::LAWICEL:
|
case CANCon::LAWICEL:
|
||||||
|
case CANCon::GS_USB:
|
||||||
return ui->cbPort->currentText();
|
return ui->cbPort->currentText();
|
||||||
case CANCon::KAYAK:
|
case CANCon::KAYAK:
|
||||||
return ui->cbPort->currentText();
|
return ui->cbPort->currentText();
|
||||||
@@ -394,7 +457,7 @@ int NewConnectionDialog::getSerialSpeed()
|
|||||||
|
|
||||||
int NewConnectionDialog::getBusSpeed()
|
int NewConnectionDialog::getBusSpeed()
|
||||||
{
|
{
|
||||||
if (getConnectionType() == CANCon::LAWICEL)
|
if (getConnectionType() == CANCon::LAWICEL || getConnectionType() == CANCon::GS_USB)
|
||||||
{
|
{
|
||||||
return ui->cbCANSpeed->currentText().toInt();
|
return ui->cbCANSpeed->currentText().toInt();
|
||||||
}
|
}
|
||||||
@@ -411,6 +474,7 @@ CANCon::type NewConnectionDialog::getConnectionType()
|
|||||||
if (ui->rbLawicel->isChecked()) return CANCon::LAWICEL;
|
if (ui->rbLawicel->isChecked()) return CANCon::LAWICEL;
|
||||||
if (ui->rbCANserver->isChecked()) return CANCon::CANSERVER;
|
if (ui->rbCANserver->isChecked()) return CANCon::CANSERVER;
|
||||||
if (ui->rbCanlogserver->isChecked()) return CANCon::CANLOGSERVER;
|
if (ui->rbCanlogserver->isChecked()) return CANCon::CANLOGSERVER;
|
||||||
|
if (ui->rbGSUSB->isChecked()) return CANCon::GS_USB;
|
||||||
qDebug() << "getConnectionType: error";
|
qDebug() << "getConnectionType: error";
|
||||||
|
|
||||||
return CANCon::NONE;
|
return CANCon::NONE;
|
||||||
@@ -424,7 +488,7 @@ bool NewConnectionDialog::isSerialBusAvailable()
|
|||||||
|
|
||||||
int NewConnectionDialog::getDataRate()
|
int NewConnectionDialog::getDataRate()
|
||||||
{
|
{
|
||||||
if (getConnectionType() == CANCon::LAWICEL)
|
if (getConnectionType() == CANCon::LAWICEL || getConnectionType() == CANCon::GS_USB)
|
||||||
{
|
{
|
||||||
return ui->cbDataRate->currentText().toInt();
|
return ui->cbDataRate->currentText().toInt();
|
||||||
}
|
}
|
||||||
@@ -433,6 +497,10 @@ int NewConnectionDialog::getDataRate()
|
|||||||
|
|
||||||
bool NewConnectionDialog::isCanFd()
|
bool NewConnectionDialog::isCanFd()
|
||||||
{
|
{
|
||||||
|
if (getConnectionType() == CANCon::GS_USB)
|
||||||
|
{
|
||||||
|
return ui->cbCanFd->isChecked();
|
||||||
|
}
|
||||||
if (getConnectionType() == CANCon::LAWICEL)
|
if (getConnectionType() == CANCon::LAWICEL)
|
||||||
{
|
{
|
||||||
return ui->cbCanFd;
|
return ui->cbCanFd;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ private:
|
|||||||
void selectLawicel();
|
void selectLawicel();
|
||||||
void selectCANserver();
|
void selectCANserver();
|
||||||
void selectCANlogserver();
|
void selectCANlogserver();
|
||||||
|
void selectGSUSB();
|
||||||
bool isSerialBusAvailable();
|
bool isSerialBusAvailable();
|
||||||
void setPortName(CANCon::type pType, QString pPortName, QString pDriver);
|
void setPortName(CANCon::type pType, QString pPortName, QString pDriver);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,25 @@ Consult the SocketCAN documentation for details on configuring such devices.
|
|||||||
|
|
||||||
QT also includes a "virtualcan" device type. You can use this to create a bus that will loop back anything you send to it. This is useful for testing without needing to connect any devices or load any log files.
|
QT also includes a "virtualcan" device type. You can use this to create a bus that will loop back anything you send to it. This is useful for testing without needing to connect any devices or load any log files.
|
||||||
|
|
||||||
|
Connecting to gs_usb Devices (Windows)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
gs_usb is the USB protocol spoken by candleLight, CANable (candleLight firmware), CANnectivity,
|
||||||
|
cantact and similar open hardware adapters. On LINUX and macOS these devices are handled by the
|
||||||
|
kernel gs_usb driver and show up as ordinary SocketCAN interfaces, so use the "QT SerialBus
|
||||||
|
Devices" option there. Windows has no such driver, which is why SavvyCAN talks to them directly
|
||||||
|
over WinUSB.
|
||||||
|
|
||||||
|
Select "gs_usb", pick your device from the list, choose a bus speed and, for FD capable hardware,
|
||||||
|
tick CAN FD and pick a data rate. Multi channel adapters appear as a single connection with one
|
||||||
|
SavvyCAN bus per channel, each of which can be configured separately once the connection exists.
|
||||||
|
|
||||||
|
The device interface has to be bound to WinUSB. Recent candleLight and CANnectivity firmware
|
||||||
|
declares this itself through WCID descriptors, so Windows installs the driver automatically when
|
||||||
|
the adapter is first plugged in. Older firmware may need the WinUSB driver assigned manually with
|
||||||
|
a tool such as Zadig. If the device does not show up in the list, that binding is the first thing
|
||||||
|
to check.
|
||||||
|
|
||||||
Connecting to Socketcand
|
Connecting to Socketcand
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="8" column="0">
|
||||||
|
<widget class="QRadioButton" name="rbGSUSB">
|
||||||
|
<property name="text">
|
||||||
|
<string>gs_usb (candleLight, CANable, CANnectivity)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user