Compare commits
2 Commits
def3eb08f3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 795a1279b1 | |||
| 80ba17d77d |
@@ -50,6 +50,9 @@ templates/
|
||||
|
||||
| Модуль | Что делает | Зависимости |
|
||||
|---|---|---|
|
||||
| [`python/setprotocol/firmware_database.py`](python/setprotocol/firmware_database.py) | база прошивок: HTTPS-каталог, скачивание с SHA-256 и кэшем, публикация в Gitea без SETGUI; [подключение и CLI](tools/firmware-publish/DATABASE.md) | stdlib, Python 3.10+ |
|
||||
| [`python/setprotocol/firmware_publish.py`](python/setprotocol/firmware_publish.py) | общие метаданные публикации, SHA-256, release tag и обновление каталога прошивок; [подключение](tools/firmware-publish/PORTING.md) | stdlib; сетевой адаптер в SETGUI |
|
||||
| [`python/setprotocol/firmware_catalog.py`](python/setprotocol/firmware_catalog.py) | модель и parser каталога `firmware.releases` | stdlib |
|
||||
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
|
||||
|
||||
|
||||
9
c/candle/CMakeLists.txt
Normal file
9
c/candle/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(candle C)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "Candle transport requires Windows WinUSB")
|
||||
endif()
|
||||
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
|
||||
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
|
||||
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
|
||||
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
66
c/candle/LICENSE
Normal file
66
c/candle/LICENSE
Normal file
File diff suppressed because one or more lines are too long
16
c/candle/README.md
Normal file
16
c/candle/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Candle / gs_usb — порт WinUSB
|
||||
|
||||
Общий C-порт для адаптеров candleLight/gs_usb. Перенесён из
|
||||
SETGUI `src/gui_desktop/native/candle_src`; исходные файлы и LGPLv3
|
||||
`LICENSE` сохранены без изменений.
|
||||
|
||||
Сборка из Developer Command Prompt:
|
||||
|
||||
```bat
|
||||
cmake -S c/candle -B build/candle -A x64
|
||||
cmake --build build/candle --config Release
|
||||
```
|
||||
|
||||
Для 32-битной библиотеки используйте `-A Win32` и отдельный каталог сборки.
|
||||
Python Qt-порт находится в `python/set_devices/qt_ports/candle_adapter.py`;
|
||||
путь к библиотеке задаётся через `CANDLE_LIBRARY`.
|
||||
1151
c/candle/candle.c
Normal file
1151
c/candle/candle.c
Normal file
@@ -0,0 +1,1151 @@
|
||||
/*
|
||||
|
||||
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.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "candle_defs.h"
|
||||
#include "candle_ctrl_req.h"
|
||||
#include "ch_9.h"
|
||||
|
||||
static bool candle_dev_interal_open(candle_handle hdev);
|
||||
|
||||
candle_log_fn_t candle_log_fn = NULL;
|
||||
bool candle_log_verbose = false;
|
||||
|
||||
static void candle_logf(const wchar_t *fmt, ...)
|
||||
{
|
||||
if (candle_log_fn == NULL) {
|
||||
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 void candle_logf_verbose(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 candle_read_di(HDEVINFO hdi, SP_DEVICE_INTERFACE_DATA interfaceData, candle_device_t *dev)
|
||||
{
|
||||
/* get required length first (this call always fails with an error) */
|
||||
ULONG requiredLength=0;
|
||||
SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, NULL, 0, &requiredLength, NULL);
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
||||
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS;
|
||||
return false;
|
||||
}
|
||||
|
||||
PSP_DEVICE_INTERFACE_DETAIL_DATA detail_data =
|
||||
(PSP_DEVICE_INTERFACE_DETAIL_DATA) LocalAlloc(LMEM_FIXED, requiredLength);
|
||||
|
||||
if (detail_data != NULL) {
|
||||
detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
|
||||
} else {
|
||||
dev->last_error = CANDLE_ERR_MALLOC;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool retval = true;
|
||||
ULONG length = requiredLength;
|
||||
if (!SetupDiGetDeviceInterfaceDetail(hdi, &interfaceData, detail_data, length, &requiredLength, NULL) ) {
|
||||
dev->last_error = CANDLE_ERR_SETUPDI_IF_DETAILS2;
|
||||
retval = false;
|
||||
} else if (FAILED(StringCchCopy(dev->path, sizeof(dev->path), detail_data->DevicePath))) {
|
||||
dev->last_error = CANDLE_ERR_PATH_LEN;
|
||||
retval = false;
|
||||
}
|
||||
|
||||
LocalFree(detail_data);
|
||||
|
||||
if (!retval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* try to open to read device infos and see if it is avail */
|
||||
if (candle_dev_interal_open(dev)) {
|
||||
dev->state = CANDLE_DEVSTATE_AVAIL;
|
||||
candle_dev_close(dev);
|
||||
} else {
|
||||
dev->state = CANDLE_DEVSTATE_INUSE;
|
||||
}
|
||||
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Return true when path already appears in l->dev[0..count-1]. */
|
||||
static bool candle_path_exists(const candle_list_t *l, unsigned count, const wchar_t *path)
|
||||
{
|
||||
for (unsigned i = 0; i < count; i++) {
|
||||
if (wcscmp(l->dev[i].path, path) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Scan one GUID and append found devices to l->dev[] starting at offset.
|
||||
* Returns the number of devices appended, or -1 on a hard error (l->last_error set). */
|
||||
static int candle_scan_guid(candle_list_t *l, const wchar_t *guid_str, unsigned offset)
|
||||
{
|
||||
GUID guid;
|
||||
if (CLSIDFromString(guid_str, &guid) != NOERROR) {
|
||||
l->last_error = CANDLE_ERR_CLSID;
|
||||
return -1;
|
||||
}
|
||||
|
||||
HDEVINFO hdi = SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
||||
if (hdi == INVALID_HANDLE_VALUE) {
|
||||
/* No devices with this GUID present — not a hard error. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int found = 0;
|
||||
for (unsigned i = 0; (offset + i) < CANDLE_MAX_DEVICES; i++) {
|
||||
SP_DEVICE_INTERFACE_DATA interfaceData;
|
||||
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
|
||||
|
||||
if (!SetupDiEnumDeviceInterfaces(hdi, NULL, &guid, i, &interfaceData)) {
|
||||
if (GetLastError() != ERROR_NO_MORE_ITEMS) {
|
||||
l->last_error = CANDLE_ERR_SETUPDI_IF_ENUM;
|
||||
found = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!candle_read_di(hdi, interfaceData, &l->dev[offset + i])) {
|
||||
l->last_error = l->dev[offset + i].last_error;
|
||||
found = -1;
|
||||
break;
|
||||
}
|
||||
found++;
|
||||
}
|
||||
|
||||
SetupDiDestroyDeviceInfoList(hdi);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Scan for WinUSB devices matching vid:pid whose device interface GUID was not
|
||||
* covered by the GUID list above. For each matching USB device instance the
|
||||
* function reads DeviceInterfaceGUIDs (or DeviceInterfaceGUID) from the Windows
|
||||
* registry, re-uses candle_scan_guid() for each GUID found there, and appends
|
||||
* only those devices that are not already present in l->dev[0..existing-1].
|
||||
* Returns the number of new devices added. */
|
||||
static int candle_scan_vidpid(candle_list_t *l, uint16_t vid, uint16_t pid, unsigned existing)
|
||||
{
|
||||
wchar_t hwid_prefix[32];
|
||||
StringCchPrintfW(hwid_prefix, 32, L"USB\\VID_%04X&PID_%04X", vid, pid);
|
||||
|
||||
/* Enumerate USB device instances (not interfaces) so we can read hardware IDs. */
|
||||
HDEVINFO hdi = SetupDiGetClassDevs(NULL, L"USB", NULL,
|
||||
DIGCF_ALLCLASSES | DIGCF_PRESENT);
|
||||
if (hdi == INVALID_HANDLE_VALUE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int added = 0;
|
||||
SP_DEVINFO_DATA devInfo;
|
||||
devInfo.cbSize = sizeof(SP_DEVINFO_DATA);
|
||||
|
||||
for (DWORD i = 0;
|
||||
SetupDiEnumDeviceInfo(hdi, i, &devInfo) && existing + added < CANDLE_MAX_DEVICES;
|
||||
i++)
|
||||
{
|
||||
|
||||
/* Hardware IDs are a REG_MULTI_SZ — check each string for our VID/PID prefix. */
|
||||
wchar_t hwids[512];
|
||||
memset(hwids, 0, sizeof(hwids));
|
||||
if (!SetupDiGetDeviceRegistryPropertyW(hdi, &devInfo, SPDRP_HARDWAREID,
|
||||
NULL, (PBYTE)hwids, sizeof(hwids) - sizeof(wchar_t), NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool matches = false;
|
||||
const wchar_t *p;
|
||||
for (p = hwids; *p; p += wcslen(p) + 1) {
|
||||
if (_wcsnicmp(p, hwid_prefix, wcslen(hwid_prefix)) == 0) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Open the device's software registry key (Device Parameters) and read
|
||||
* the WinUSB device interface GUID(s) stored by the driver INF. */
|
||||
HKEY hKey = SetupDiOpenDevRegKey(hdi, &devInfo, DICS_FLAG_GLOBAL, 0,
|
||||
DIREG_DEV, KEY_READ);
|
||||
if (hKey == INVALID_HANDLE_VALUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wchar_t guid_buf[256];
|
||||
memset(guid_buf, 0, sizeof(guid_buf));
|
||||
DWORD buf_len = sizeof(guid_buf) - sizeof(wchar_t);
|
||||
|
||||
/* Prefer DeviceInterfaceGUIDs (REG_MULTI_SZ, modern INFs); fall back to
|
||||
* DeviceInterfaceGUID (REG_SZ, older/zadig-generated INFs). */
|
||||
LONG reg_rc = RegQueryValueExW(hKey, L"DeviceInterfaceGUIDs", NULL, NULL,
|
||||
(LPBYTE)guid_buf, &buf_len);
|
||||
if (reg_rc != ERROR_SUCCESS) {
|
||||
buf_len = sizeof(guid_buf) - sizeof(wchar_t);
|
||||
RegQueryValueExW(hKey, L"DeviceInterfaceGUID", NULL, NULL,
|
||||
(LPBYTE)guid_buf, &buf_len);
|
||||
}
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (!guid_buf[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Iterate GUID strings. Both REG_SZ and REG_MULTI_SZ are covered by the
|
||||
* same NUL-terminated-string walk (REG_SZ just has one entry). */
|
||||
const wchar_t *g;
|
||||
for (g = guid_buf;
|
||||
*g && existing + added < CANDLE_MAX_DEVICES;
|
||||
g += wcslen(g) + 1)
|
||||
{
|
||||
unsigned base = existing + added;
|
||||
int n = candle_scan_guid(l, g, base);
|
||||
if (n <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Remove any entries whose path was already found by the GUID scan. */
|
||||
for (int ni = 0; ni < n; ) {
|
||||
if (candle_path_exists(l, base, l->dev[base + ni].path)) {
|
||||
memmove(&l->dev[base + ni], &l->dev[base + ni + 1],
|
||||
(unsigned)(n - ni - 1) * sizeof(candle_device_t));
|
||||
n--;
|
||||
} else {
|
||||
ni++;
|
||||
}
|
||||
}
|
||||
added += n;
|
||||
}
|
||||
}
|
||||
|
||||
SetupDiDestroyDeviceInfoList(hdi);
|
||||
return added;
|
||||
}
|
||||
|
||||
bool __stdcall candle_list_scan(candle_list_handle *list)
|
||||
{
|
||||
if (list == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
candle_list_t *l = (candle_list_t *)calloc(1, sizeof(candle_list_t));
|
||||
*list = l;
|
||||
if (l == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* GUIDs for gs_usb-compatible devices on Windows.
|
||||
* candleLight / CANable / most gs_usb devices: */
|
||||
static const wchar_t *GUIDS[] = {
|
||||
L"{c15b4308-04d3-11e6-b3ea-6057189e6443}" /* candleLight / CANable / gs_usb standard */
|
||||
};
|
||||
static const unsigned NUM_GUIDS = sizeof(GUIDS) / sizeof(GUIDS[0]);
|
||||
|
||||
unsigned total = 0;
|
||||
for (unsigned g = 0; g < NUM_GUIDS; g++) {
|
||||
int n = candle_scan_guid(l, GUIDS[g], total);
|
||||
if (n < 0) {
|
||||
return false;
|
||||
}
|
||||
total += (unsigned)n;
|
||||
}
|
||||
|
||||
/* VID/PID scan for devices whose device interface GUID is not in the list
|
||||
* above (e.g. CANnectivity which uses its own registered interface GUID). */
|
||||
static const struct { uint16_t vid; uint16_t pid; } VIDPIDS[] = {
|
||||
{ 0x1209, 0xCA01 }, /* CANnectivity (electronut-labs) */
|
||||
};
|
||||
static const unsigned NUM_VIDPIDS = sizeof(VIDPIDS) / sizeof(VIDPIDS[0]);
|
||||
|
||||
for (unsigned v = 0; v < NUM_VIDPIDS && total < CANDLE_MAX_DEVICES; v++) {
|
||||
int n = candle_scan_vidpid(l, VIDPIDS[v].vid, VIDPIDS[v].pid, total);
|
||||
if (n > 0)
|
||||
total += (unsigned)n;
|
||||
}
|
||||
|
||||
l->num_devices = (uint8_t)total;
|
||||
l->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_list_free(candle_list_handle list)
|
||||
{
|
||||
free(list);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len)
|
||||
{
|
||||
candle_list_t *l = (candle_list_t *)list;
|
||||
*len = l->num_devices;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev)
|
||||
{
|
||||
candle_list_t *l = (candle_list_t *)list;
|
||||
if (l==NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dev_num >= CANDLE_MAX_DEVICES) {
|
||||
l->last_error = CANDLE_ERR_DEV_OUT_OF_RANGE;
|
||||
return false;
|
||||
}
|
||||
|
||||
candle_device_t *dev = calloc(1, sizeof(candle_device_t));
|
||||
*hdev = dev;
|
||||
if (dev==NULL) {
|
||||
l->last_error = CANDLE_ERR_MALLOC;
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(dev, &l->dev[dev_num], sizeof(candle_device_t));
|
||||
l->last_error = CANDLE_ERR_OK;
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state)
|
||||
{
|
||||
if (hdev==NULL) {
|
||||
return false;
|
||||
} else {
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
*state = dev->state;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev)
|
||||
{
|
||||
if (hdev==NULL) {
|
||||
return NULL;
|
||||
} else {
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return dev->path;
|
||||
}
|
||||
}
|
||||
|
||||
static bool candle_dev_interal_open(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
memset(dev->rxevents, 0, sizeof(dev->rxevents));
|
||||
memset(dev->rxurbs, 0, sizeof(dev->rxurbs));
|
||||
|
||||
dev->deviceHandle = CreateFile(
|
||||
dev->path,
|
||||
GENERIC_WRITE | GENERIC_READ,
|
||||
FILE_SHARE_WRITE | FILE_SHARE_READ,
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (dev->deviceHandle == INVALID_HANDLE_VALUE) {
|
||||
dev->last_error = CANDLE_ERR_CREATE_FILE;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!WinUsb_Initialize(dev->deviceHandle, &dev->winUSBHandle)) {
|
||||
dev->last_error = CANDLE_ERR_WINUSB_INITIALIZE;
|
||||
goto close_handle;
|
||||
}
|
||||
|
||||
USB_INTERFACE_DESCRIPTOR ifaceDescriptor;
|
||||
if (!WinUsb_QueryInterfaceSettings(dev->winUSBHandle, 0, &ifaceDescriptor)) {
|
||||
dev->last_error = CANDLE_ERR_QUERY_INTERFACE;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
dev->interfaceNumber = ifaceDescriptor.bInterfaceNumber;
|
||||
bool has_in = false, has_out = false;
|
||||
|
||||
candle_logf(L"open path=%ls interface=%u endpoints=%u",
|
||||
dev->path,
|
||||
dev->interfaceNumber,
|
||||
ifaceDescriptor.bNumEndpoints);
|
||||
|
||||
for (uint8_t i=0; i<ifaceDescriptor.bNumEndpoints; i++) {
|
||||
|
||||
WINUSB_PIPE_INFORMATION pipeInfo;
|
||||
if (!WinUsb_QueryPipe(dev->winUSBHandle, 0, i, &pipeInfo)) {
|
||||
dev->last_error = CANDLE_ERR_QUERY_PIPE;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_IN(pipeInfo.PipeId)) {
|
||||
if (!has_in) {
|
||||
dev->bulkInPipe = pipeInfo.PipeId;
|
||||
has_in = true;
|
||||
candle_logf(L"selected bulk IN pipe=0x%02x maxPacket=%u interval=%u",
|
||||
pipeInfo.PipeId,
|
||||
pipeInfo.MaximumPacketSize,
|
||||
pipeInfo.Interval);
|
||||
}
|
||||
} else if (pipeInfo.PipeType == UsbdPipeTypeBulk && USB_ENDPOINT_DIRECTION_OUT(pipeInfo.PipeId)) {
|
||||
if (!has_out) {
|
||||
dev->bulkOutPipe = pipeInfo.PipeId;
|
||||
has_out = true;
|
||||
candle_logf(L"selected bulk OUT pipe=0x%02x maxPacket=%u interval=%u",
|
||||
pipeInfo.PipeId,
|
||||
pipeInfo.MaximumPacketSize,
|
||||
pipeInfo.Interval);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!has_in || !has_out) {
|
||||
dev->last_error = CANDLE_ERR_PARSE_IF_DESCR;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
char use_raw_io = 1;
|
||||
if (!WinUsb_SetPipePolicy(dev->winUSBHandle, dev->bulkInPipe, RAW_IO, sizeof(use_raw_io), &use_raw_io)) {
|
||||
dev->last_error = CANDLE_ERR_SET_PIPE_RAW_IO;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
if (!candle_ctrl_set_host_format(dev)) {
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
if (!candle_ctrl_get_config(dev, &dev->dconf)) {
|
||||
goto winusb_free;
|
||||
}
|
||||
candle_logf(L"device config channels=%u sw=0x%08x hw=0x%08x",
|
||||
dev->dconf.icount + 1,
|
||||
dev->dconf.sw_version,
|
||||
dev->dconf.hw_version);
|
||||
|
||||
if (!candle_ctrl_get_capability(dev, 0, &dev->bt_const)) {
|
||||
dev->last_error = CANDLE_ERR_GET_BITTIMING_CONST;
|
||||
goto winusb_free;
|
||||
}
|
||||
candle_logf(L"cap ch0 feature=0x%08x fclk=%u tseg1=%u..%u tseg2=%u..%u sjw=%u brp=%u..%u inc=%u",
|
||||
dev->bt_const.feature,
|
||||
dev->bt_const.fclk_can,
|
||||
dev->bt_const.tseg1_min,
|
||||
dev->bt_const.tseg1_max,
|
||||
dev->bt_const.tseg2_min,
|
||||
dev->bt_const.tseg2_max,
|
||||
dev->bt_const.sjw_max,
|
||||
dev->bt_const.brp_min,
|
||||
dev->bt_const.brp_max,
|
||||
dev->bt_const.brp_inc);
|
||||
|
||||
/* Query capabilities for each channel on multi-channel devices */
|
||||
uint8_t num_channels = dev->dconf.icount + 1;
|
||||
if (num_channels > 8) num_channels = 8;
|
||||
for (uint8_t ch = 0; ch < num_channels; ch++) {
|
||||
if (!candle_ctrl_get_capability(dev, ch, &dev->ch_caps[ch])) {
|
||||
/* Fall back to channel 0 capabilities for this channel */
|
||||
memcpy(&dev->ch_caps[ch], &dev->bt_const, sizeof(candle_capability_t));
|
||||
candle_logf(L"cap ch%u failed, falling back to ch0", ch);
|
||||
} else {
|
||||
candle_logf(L"cap ch%u feature=0x%08x fclk=%u",
|
||||
ch,
|
||||
dev->ch_caps[ch].feature,
|
||||
dev->ch_caps[ch].fclk_can);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pre-allocate a manual-reset event for timed overlapped writes. Reusing
|
||||
* one event per device (writes are serialised by writeMutex) avoids
|
||||
* per-frame CreateEvent overhead at high CAN frame rates. */
|
||||
dev->txEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
|
||||
if (!dev->txEvent) {
|
||||
dev->last_error = CANDLE_ERR_MALLOC;
|
||||
goto winusb_free;
|
||||
}
|
||||
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
|
||||
winusb_free:
|
||||
WinUsb_Free(dev->winUSBHandle);
|
||||
dev->winUSBHandle = NULL;
|
||||
|
||||
close_handle:
|
||||
CloseHandle(dev->deviceHandle);
|
||||
dev->deviceHandle = NULL;
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
static bool candle_prepare_read(candle_device_t *dev, unsigned urb_num)
|
||||
{
|
||||
if (dev->rxurbs[urb_num].pending) {
|
||||
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dev->rxurbs[urb_num].ovl.hEvent == NULL) {
|
||||
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||
return false;
|
||||
}
|
||||
|
||||
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||
|
||||
BOOL rc = WinUsb_ReadPipe(
|
||||
dev->winUSBHandle,
|
||||
dev->bulkInPipe,
|
||||
dev->rxurbs[urb_num].buf,
|
||||
sizeof(dev->rxurbs[urb_num].buf),
|
||||
NULL,
|
||||
&dev->rxurbs[urb_num].ovl
|
||||
);
|
||||
|
||||
if (rc) {
|
||||
/* Synchronous completion: data is already in buf and the event is
|
||||
* signaled. WaitForMultipleObjects will return immediately on the
|
||||
* next call and GetOverlappedResult will succeed, so this is fine. */
|
||||
dev->rxurbs[urb_num].pending = true;
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_IO_PENDING) {
|
||||
dev->rxurbs[urb_num].pending = true;
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
candle_logf(L"prepare read urb=%u failed winerr=%lu", urb_num, err);
|
||||
dev->last_error = CANDLE_ERR_PREPARE_READ;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool candle_close_rxurbs(candle_device_t *dev)
|
||||
{
|
||||
if (dev->winUSBHandle != NULL) {
|
||||
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkInPipe);
|
||||
}
|
||||
|
||||
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
|
||||
if (dev->rxurbs[i].pending) {
|
||||
CancelIoEx(dev->deviceHandle, &dev->rxurbs[i].ovl);
|
||||
|
||||
DWORD bytes_transfered;
|
||||
WinUsb_GetOverlappedResult(dev->winUSBHandle,
|
||||
&dev->rxurbs[i].ovl,
|
||||
&bytes_transfered,
|
||||
TRUE);
|
||||
dev->rxurbs[i].pending = false;
|
||||
}
|
||||
|
||||
if (dev->rxevents[i] != NULL) {
|
||||
CloseHandle(dev->rxevents[i]);
|
||||
dev->rxevents[i] = NULL;
|
||||
memset(&dev->rxurbs[i].ovl, 0, sizeof(dev->rxurbs[i].ovl));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void candle_release_open_handles(candle_device_t *dev)
|
||||
{
|
||||
candle_close_rxurbs(dev);
|
||||
|
||||
if (dev->txEvent) {
|
||||
CloseHandle(dev->txEvent);
|
||||
dev->txEvent = NULL;
|
||||
}
|
||||
|
||||
if (dev->winUSBHandle) {
|
||||
WinUsb_Free(dev->winUSBHandle);
|
||||
dev->winUSBHandle = NULL;
|
||||
}
|
||||
|
||||
if (dev->deviceHandle && dev->deviceHandle != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(dev->deviceHandle);
|
||||
dev->deviceHandle = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool __stdcall DLL candle_dev_open(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
if (candle_dev_interal_open(dev)) {
|
||||
for (unsigned i=0; i<CANDLE_URB_COUNT; i++) {
|
||||
HANDLE ev = CreateEvent(NULL, true, false, NULL);
|
||||
if (ev == NULL) {
|
||||
dev->last_error = CANDLE_ERR_MALLOC;
|
||||
candle_err_t last_error = dev->last_error;
|
||||
candle_release_open_handles(dev);
|
||||
dev->last_error = last_error;
|
||||
return false;
|
||||
}
|
||||
dev->rxevents[i] = ev;
|
||||
dev->rxurbs[i].ovl.hEvent = ev;
|
||||
if (!candle_prepare_read(dev, i)) {
|
||||
candle_err_t last_error = dev->last_error;
|
||||
candle_release_open_handles(dev);
|
||||
dev->last_error = last_error;
|
||||
return false; // keep last_error from prepare_read call
|
||||
}
|
||||
}
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
} else {
|
||||
return false; // keep last_error from open_device call
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
|
||||
{
|
||||
return candle_ctrl_get_timestamp(hdev, timestamp_us);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_close(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
candle_release_open_handles(dev);
|
||||
|
||||
dev->last_error = CANDLE_ERR_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_dev_free(candle_handle hdev)
|
||||
{
|
||||
free(hdev);
|
||||
return true;
|
||||
}
|
||||
|
||||
candle_err_t __stdcall DLL candle_dev_last_error(candle_handle hdev)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return dev->last_error;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_count(candle_handle hdev, uint8_t *num_channels)
|
||||
{
|
||||
// TODO check if info was already read from device; try to do so; throw error...
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
*num_channels = dev->dconf.icount+1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_get_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
uint8_t num_channels = dev->dconf.icount + 1;
|
||||
if (ch < num_channels && ch < 8) {
|
||||
memcpy(cap, &dev->ch_caps[ch], sizeof(candle_capability_t));
|
||||
} else {
|
||||
memcpy(cap, &dev->bt_const, sizeof(candle_capability_t));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_get_state(candle_handle hdev, uint8_t ch, candle_can_state_t *state)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
candle_device_state_t ds;
|
||||
if (!candle_ctrl_get_state(dev, ch, &ds)) {
|
||||
return false;
|
||||
}
|
||||
*state = (candle_can_state_t)ds.state;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_bus_off_recover(candle_handle hdev, uint8_t ch)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_bus_off_recover(dev, ch);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_set_bittiming(dev, ch, data);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
if (dev->bt_const.fclk_can != 48000000) {
|
||||
/* this function only works for the candleLight base clock of 48MHz */
|
||||
dev->last_error = CANDLE_ERR_BITRATE_FCLK;
|
||||
return false;
|
||||
}
|
||||
|
||||
candle_bittiming_t t;
|
||||
t.prop_seg = 1;
|
||||
t.sjw = 1;
|
||||
t.phase_seg1 = 13 - t.prop_seg;
|
||||
t.phase_seg2 = 2;
|
||||
|
||||
switch (bitrate) {
|
||||
case 10000:
|
||||
t.brp = 300;
|
||||
break;
|
||||
|
||||
case 20000:
|
||||
t.brp = 150;
|
||||
break;
|
||||
|
||||
case 50000:
|
||||
t.brp = 60;
|
||||
break;
|
||||
|
||||
case 83333:
|
||||
t.brp = 36;
|
||||
break;
|
||||
|
||||
case 100000:
|
||||
t.brp = 30;
|
||||
break;
|
||||
|
||||
case 125000:
|
||||
t.brp = 24;
|
||||
break;
|
||||
|
||||
case 250000:
|
||||
t.brp = 12;
|
||||
break;
|
||||
|
||||
case 500000:
|
||||
t.brp = 6;
|
||||
break;
|
||||
|
||||
case 800000:
|
||||
t.brp = 4;
|
||||
t.phase_seg1 = 12 - t.prop_seg;
|
||||
t.phase_seg2 = 2;
|
||||
break;
|
||||
|
||||
case 1000000:
|
||||
t.brp = 3;
|
||||
break;
|
||||
|
||||
default:
|
||||
dev->last_error = CANDLE_ERR_BITRATE_UNSUPPORTED;
|
||||
return false;
|
||||
}
|
||||
|
||||
return candle_ctrl_set_bittiming(dev, ch, &t);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
candle_capability_t *cap = (ch < 8) ? &dev->ch_caps[ch] : &dev->bt_const;
|
||||
|
||||
if (cap->feature & CANDLE_FEATURE_HW_TIMESTAMP) {
|
||||
flags |= CANDLE_MODE_HW_TIMESTAMP;
|
||||
} else {
|
||||
candle_logf(L"channel %u has no HW timestamp capability; starting without timestamp flag", ch);
|
||||
}
|
||||
|
||||
bool rc = candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_START, flags);
|
||||
candle_logf(L"channel %u start flags=0x%08x result=%u err=%u",
|
||||
ch,
|
||||
flags,
|
||||
rc ? 1 : 0,
|
||||
dev->last_error);
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
|
||||
{
|
||||
// TODO ensure device is open, check channel count..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
|
||||
}
|
||||
|
||||
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
|
||||
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
|
||||
* is never accessed by two threads simultaneously. */
|
||||
static bool candle_write_pipe_timed(candle_device_t *dev, uint8_t *buf, DWORD len)
|
||||
{
|
||||
OVERLAPPED ovl;
|
||||
memset(&ovl, 0, sizeof(ovl));
|
||||
ovl.hEvent = dev->txEvent;
|
||||
ResetEvent(dev->txEvent);
|
||||
|
||||
BOOL rc = WinUsb_WritePipe(dev->winUSBHandle, dev->bulkOutPipe,
|
||||
buf, len, NULL, &ovl);
|
||||
if (rc) {
|
||||
return true; /* completed synchronously */
|
||||
}
|
||||
if (GetLastError() != ERROR_IO_PENDING) {
|
||||
return false; /* hard error */
|
||||
}
|
||||
|
||||
if (WaitForSingleObject(dev->txEvent, 150) != WAIT_OBJECT_0) {
|
||||
/* Timed out: cancel the transfer and restore the pipe to a clean state. */
|
||||
WinUsb_AbortPipe(dev->winUSBHandle, dev->bulkOutPipe);
|
||||
DWORD dummy = 0;
|
||||
WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &dummy, TRUE);
|
||||
WinUsb_ResetPipe(dev->winUSBHandle, dev->bulkOutPipe);
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD transferred = 0;
|
||||
return WinUsb_GetOverlappedResult(dev->winUSBHandle, &ovl, &transferred, FALSE) != FALSE;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
frame->echo_id = 0;
|
||||
frame->channel = ch;
|
||||
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms)
|
||||
{
|
||||
// TODO ensure device is open..
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
|
||||
if (wait_result == WAIT_TIMEOUT) {
|
||||
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
|
||||
dev->last_error = CANDLE_ERR_READ_WAIT;
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD urb_num = wait_result - WAIT_OBJECT_0;
|
||||
DWORD bytes_transfered;
|
||||
|
||||
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_IO_INCOMPLETE) {
|
||||
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||
} else {
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
candle_logf(L"classic read result failed urb=%u winerr=%lu", urb_num, err);
|
||||
dev->last_error = CANDLE_ERR_READ_RESULT;
|
||||
return false;
|
||||
}
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
|
||||
if (bytes_transfered < sizeof(*frame)-4) {
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"classic read too small urb=%u bytes=%lu min=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
(unsigned)(sizeof(*frame) - 4));
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
DWORD copy_len = (bytes_transfered < sizeof(*frame)) ? bytes_transfered : sizeof(*frame);
|
||||
memcpy(frame, dev->rxurbs[urb_num].buf, copy_len);
|
||||
candle_logf_verbose(L"classic read urb=%u bytes=%lu echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
frame->echo_id,
|
||||
frame->can_id,
|
||||
frame->can_dlc,
|
||||
frame->channel,
|
||||
frame->flags,
|
||||
frame->timestamp_us);
|
||||
|
||||
return candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
|
||||
candle_frametype_t __stdcall DLL candle_frame_type(candle_frame_t *frame)
|
||||
{
|
||||
if (frame->echo_id != 0xFFFFFFFF) {
|
||||
return CANDLE_FRAMETYPE_ECHO;
|
||||
};
|
||||
|
||||
if (frame->can_id & CANDLE_ID_ERR) {
|
||||
return CANDLE_FRAMETYPE_ERROR;
|
||||
}
|
||||
|
||||
return CANDLE_FRAMETYPE_RECEIVE;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_frame_id(candle_frame_t *frame)
|
||||
{
|
||||
return frame->can_id & 0x1FFFFFFF;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_is_extended_id(candle_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_frame_is_rtr(candle_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_RTR) != 0;
|
||||
}
|
||||
|
||||
uint8_t __stdcall DLL candle_frame_dlc(candle_frame_t *frame)
|
||||
{
|
||||
return frame->can_dlc;
|
||||
}
|
||||
|
||||
uint8_t * __stdcall DLL candle_frame_data(candle_frame_t *frame)
|
||||
{
|
||||
return frame->data;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_frame_timestamp_us(candle_frame_t *frame)
|
||||
{
|
||||
return frame->timestamp_us;
|
||||
}
|
||||
|
||||
/* ---- CAN FD extensions ---- */
|
||||
|
||||
bool __stdcall DLL candle_channel_set_data_timing(candle_handle hdev, uint8_t ch, candle_bittiming_t *data)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
return candle_ctrl_set_data_bittiming(dev, ch, data);
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_send(candle_handle hdev, uint8_t ch, candle_fd_frame_t *frame)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
frame->echo_id = 0;
|
||||
frame->channel = ch;
|
||||
bool rc = candle_write_pipe_timed(dev, (uint8_t*)frame, sizeof(*frame));
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SEND_FRAME;
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *frame, uint32_t timeout_ms)
|
||||
{
|
||||
candle_device_t *dev = (candle_device_t*)hdev;
|
||||
|
||||
DWORD wait_result = WaitForMultipleObjects(CANDLE_URB_COUNT, dev->rxevents, false, timeout_ms);
|
||||
if (wait_result == WAIT_TIMEOUT) {
|
||||
dev->last_error = CANDLE_ERR_READ_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( (wait_result < WAIT_OBJECT_0) || (wait_result >= WAIT_OBJECT_0 + CANDLE_URB_COUNT) ) {
|
||||
dev->last_error = CANDLE_ERR_READ_WAIT;
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD urb_num = wait_result - WAIT_OBJECT_0;
|
||||
DWORD bytes_transfered;
|
||||
|
||||
if (!WinUsb_GetOverlappedResult(dev->winUSBHandle, &dev->rxurbs[urb_num].ovl, &bytes_transfered, false)) {
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_IO_INCOMPLETE) {
|
||||
ResetEvent(dev->rxurbs[urb_num].ovl.hEvent);
|
||||
} else {
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
candle_logf(L"fd read result failed urb=%u winerr=%lu", urb_num, err);
|
||||
dev->last_error = CANDLE_ERR_READ_RESULT;
|
||||
return false;
|
||||
}
|
||||
dev->rxurbs[urb_num].pending = false;
|
||||
|
||||
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
|
||||
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
|
||||
if (bytes_transfered < classic_min) {
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
classic_min);
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
|
||||
/*
|
||||
* Detect frame type from the flags byte (offset 10 in both structs).
|
||||
* Classic CAN frames: header(12) + data(8) + timestamp(4) = 24 bytes total.
|
||||
*
|
||||
* FD frames come in two wire formats:
|
||||
* - Legacy fixed (candleLight/CANable 1.x): always 80 bytes — header(12) +
|
||||
* data[64] + timestamp(4). The timestamp is ALWAYS at offset 76, regardless
|
||||
* of the actual DLC. Identified by bytes_transferred == sizeof(candle_fd_frame_t).
|
||||
* - Variable-length (CANnectivity/Zephyr): header(12) + actual_data(DLC) +
|
||||
* timestamp(4). Identified by bytes_transferred < sizeof(candle_fd_frame_t).
|
||||
*/
|
||||
bool is_fd_frame = (dev->rxurbs[urb_num].buf[10] & CANDLE_FRAME_FLAG_FD) != 0;
|
||||
|
||||
if (is_fd_frame) {
|
||||
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
|
||||
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
|
||||
const DWORD data_len = candle_dlc_to_len(raw_dlc);
|
||||
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
|
||||
|
||||
if (bytes_transfered < min_size) {
|
||||
candle_prepare_read(dev, urb_num);
|
||||
candle_logf(L"fd read FD frame too small urb=%u bytes=%lu min=%lu flags=0x%02x dlc=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
min_size,
|
||||
dev->rxurbs[urb_num].buf[10],
|
||||
raw_dlc);
|
||||
dev->last_error = CANDLE_ERR_READ_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Copy the fixed 12-byte header (echo_id … reserved). */
|
||||
memcpy(frame, dev->rxurbs[urb_num].buf, 12);
|
||||
/* Copy data at offset 12 into the struct's data field. */
|
||||
memcpy(frame->data, dev->rxurbs[urb_num].buf + 12, data_len);
|
||||
|
||||
/* Timestamp location depends on the wire format (see comment above). */
|
||||
const DWORD fixed_ts_offset = (DWORD)(sizeof(candle_fd_frame_t) - sizeof(uint32_t)); /* = 76 */
|
||||
const DWORD ts_offset = (bytes_transfered >= (DWORD)sizeof(candle_fd_frame_t))
|
||||
? fixed_ts_offset
|
||||
: min_size;
|
||||
if (bytes_transfered >= ts_offset + (DWORD)sizeof(uint32_t)) {
|
||||
memcpy(&frame->timestamp_us, dev->rxurbs[urb_num].buf + ts_offset, sizeof(uint32_t));
|
||||
}
|
||||
/* else: timestamp stays zero from memset above */
|
||||
} else {
|
||||
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
|
||||
candle_frame_t classic;
|
||||
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);
|
||||
memcpy(&classic, dev->rxurbs[urb_num].buf, copy_len);
|
||||
|
||||
frame->echo_id = classic.echo_id;
|
||||
frame->can_id = classic.can_id;
|
||||
frame->can_dlc = classic.can_dlc;
|
||||
frame->channel = classic.channel;
|
||||
frame->flags = classic.flags;
|
||||
frame->reserved = classic.reserved;
|
||||
memcpy(frame->data, classic.data, 8);
|
||||
frame->timestamp_us = (bytes_transfered >= sizeof(classic)) ? classic.timestamp_us : 0;
|
||||
}
|
||||
candle_logf_verbose(L"fd read urb=%u bytes=%lu is_fd=%u echo=0x%08x can_id=0x%08x dlc=%u ch=%u flags=0x%02x ts=%u",
|
||||
urb_num,
|
||||
bytes_transfered,
|
||||
is_fd_frame ? 1 : 0,
|
||||
frame->echo_id,
|
||||
frame->can_id,
|
||||
frame->can_dlc,
|
||||
frame->channel,
|
||||
frame->flags,
|
||||
frame->timestamp_us);
|
||||
|
||||
return candle_prepare_read(dev, urb_num);
|
||||
}
|
||||
|
||||
candle_frametype_t __stdcall DLL candle_fd_frame_type(candle_fd_frame_t *frame)
|
||||
{
|
||||
if (frame->echo_id != 0xFFFFFFFF) {
|
||||
return CANDLE_FRAMETYPE_ECHO;
|
||||
}
|
||||
if (frame->can_id & CANDLE_ID_ERR) {
|
||||
return CANDLE_FRAMETYPE_ERROR;
|
||||
}
|
||||
return CANDLE_FRAMETYPE_RECEIVE;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_fd_frame_id(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->can_id & 0x1FFFFFFF;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_extended_id(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_EXTENDED) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_rtr(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->can_id & CANDLE_ID_RTR) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_fd(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->flags & CANDLE_FRAME_FLAG_FD) != 0;
|
||||
}
|
||||
|
||||
bool __stdcall DLL candle_fd_frame_is_brs(candle_fd_frame_t *frame)
|
||||
{
|
||||
return (frame->flags & CANDLE_FRAME_FLAG_BRS) != 0;
|
||||
}
|
||||
|
||||
uint8_t __stdcall DLL candle_fd_frame_dlc(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->can_dlc;
|
||||
}
|
||||
|
||||
uint8_t * __stdcall DLL candle_fd_frame_data(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->data;
|
||||
}
|
||||
|
||||
uint32_t __stdcall DLL candle_fd_frame_timestamp_us(candle_fd_frame_t *frame)
|
||||
{
|
||||
return frame->timestamp_us;
|
||||
}
|
||||
29
c/candle/candle.def
Normal file
29
c/candle/candle.def
Normal file
@@ -0,0 +1,29 @@
|
||||
EXPORTS
|
||||
candle_list_scan
|
||||
candle_list_free
|
||||
candle_list_length
|
||||
candle_dev_get
|
||||
candle_dev_get_state
|
||||
candle_dev_get_path
|
||||
candle_dev_open
|
||||
candle_dev_get_timestamp_us
|
||||
candle_dev_close
|
||||
candle_dev_free
|
||||
candle_channel_count
|
||||
candle_channel_get_capabilities
|
||||
candle_channel_get_state
|
||||
candle_channel_bus_off_recover
|
||||
candle_channel_set_timing
|
||||
candle_channel_set_bitrate
|
||||
candle_channel_start
|
||||
candle_channel_stop
|
||||
candle_frame_send
|
||||
candle_frame_read
|
||||
candle_frame_type
|
||||
candle_frame_id
|
||||
candle_frame_is_extended_id
|
||||
candle_frame_is_rtr
|
||||
candle_frame_dlc
|
||||
candle_frame_data
|
||||
candle_frame_timestamp_us
|
||||
candle_dev_last_error
|
||||
287
c/candle/candle.h
Normal file
287
c/candle/candle.h
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
|
||||
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,
|
||||
/** Host-controlled recovery after the controller enters bus-off. */
|
||||
CANDLE_FEATURE_BUS_OFF_RECOVERY = 0x40000,
|
||||
};
|
||||
|
||||
/* 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_bus_off_recover(candle_handle hdev, uint8_t ch);
|
||||
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
|
||||
234
c/candle/candle_ctrl_req.c
Normal file
234
c/candle/candle_ctrl_req.c
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
|
||||
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,
|
||||
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
|
||||
{
|
||||
bool rc = usb_control_msg(
|
||||
dev->winUSBHandle,
|
||||
CANDLE_BREQ_BUS_OFF_RECOVERY,
|
||||
USB_DIR_OUT|USB_TYPE_VENDOR|USB_RECIP_INTERFACE,
|
||||
channel,
|
||||
dev->interfaceNumber,
|
||||
NULL,
|
||||
0
|
||||
);
|
||||
|
||||
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
|
||||
return rc;
|
||||
}
|
||||
49
c/candle/candle_ctrl_req.h
Normal file
49
c/candle/candle_ctrl_req.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
|
||||
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);
|
||||
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel);
|
||||
|
||||
101
c/candle/candle_defs.h
Normal file
101
c/candle/candle_defs.h
Normal file
@@ -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;
|
||||
|
||||
37
c/candle/ch_9.h
Normal file
37
c/candle/ch_9.h
Normal file
@@ -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
|
||||
@@ -1,11 +1,144 @@
|
||||
# PORTING
|
||||
# Портирование firmware-info
|
||||
|
||||
Аппаратно-зависимый контракт ограничен `firmware_info_config.h`:
|
||||
`FIRMWARE_VERSION_MAJOR`, `FIRMWARE_VERSION_MINOR`, `FIRMWARE_VERSION_PATCH` и
|
||||
необязательный `FIRMWARE_BUILD_ID`. Выберите порт по семейству МК или создайте
|
||||
его копию. Публикация — ответственность транспорта: 12 `uint16_t` для Modbus
|
||||
либо 24 little-endian байта для SETGUI.
|
||||
## Назначение и границы
|
||||
|
||||
При переносе проверьте: поддержку `__DATE__`/`__TIME__`, наличие generated в
|
||||
include path, запуск генератора до компиляции и декодирование build ID как двух
|
||||
ASCII-байтов в каждом логическом слове.
|
||||
Это C-библиотека описания работающего образа: SemVer, дата/время компиляции,
|
||||
8 символов build ID и сериализация контракта v1. Она не обращается к сети,
|
||||
не читает версию из сервера и не записывает Flash. HAL, RTOS, UART и CAN
|
||||
ядру не нужны. Отправку результата выполняет приложение.
|
||||
|
||||
Для размещения `.hex/.bin` в каталоге SETGUI уже существуют
|
||||
[`setprotocol.firmware_publish`](../../python/setprotocol/firmware_publish.py)
|
||||
и [`tools/firmware-publish`](../../tools/firmware-publish/README.md).
|
||||
Связь компонентов и перенос публикации описаны в
|
||||
[`tools/firmware-publish/PORTING.md`](../../tools/firmware-publish/PORTING.md).
|
||||
|
||||
## 1. Файлы и конфигурация
|
||||
|
||||
Пример ниже предполагает `lib/templates` внутри нового проекта:
|
||||
|
||||
```text
|
||||
project/
|
||||
inc/firmware_info_config.h
|
||||
generated/firmware_build_id.h # создаётся до сборки
|
||||
lib/templates/c/firmware-info/
|
||||
src/
|
||||
```
|
||||
|
||||
Добавьте в сборку оба файла:
|
||||
|
||||
```text
|
||||
lib/templates/c/firmware-info/src/firmware_info.c
|
||||
lib/templates/c/firmware-info/src/firmware_info_port.c
|
||||
```
|
||||
|
||||
В include path добавьте `inc`, `generated` и
|
||||
`lib/templates/c/firmware-info/include`. Встроенный `CMakeLists.txt` собирает
|
||||
только ядро и host-тест; `firmware_info_port.c` и каталоги конфигурации нужно
|
||||
добавлять к целевому firmware target самостоятельно.
|
||||
|
||||
Скопируйте `ports/<mcu>/firmware_info_config.template.h` как
|
||||
`inc/firmware_info_config.h`. Есть варианты STM32F1/F4/G4 и К1921ВК028;
|
||||
они не содержат регистров МК. Для другого МК с обычными 8-битными байтами
|
||||
достаточно такого же config:
|
||||
|
||||
```c
|
||||
#ifndef FIRMWARE_INFO_CONFIG_H
|
||||
#define FIRMWARE_INFO_CONFIG_H
|
||||
#define FIRMWARE_VERSION_MAJOR 1U
|
||||
#define FIRMWARE_VERSION_MINOR 2U
|
||||
#define FIRMWARE_VERSION_PATCH 3U
|
||||
#include "firmware_build_id.h"
|
||||
#endif
|
||||
```
|
||||
|
||||
Обязательный include в этом примере позволяет обнаружить пропущенный pre-build.
|
||||
В готовых шаблонах include опциональный через `__has_include`; если заголовок
|
||||
не подключён, порт использует `LOCALDEV`. Для компилятора без `__has_include`
|
||||
используйте явный include. Можно связать три версии с существующими макросами
|
||||
проекта, как это сделано в `KONOR_ds18b20/inc/firmware_info_config.h`.
|
||||
|
||||
## 2. Build ID
|
||||
|
||||
Из корня нового проекта перед компиляцией запустите:
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "lib/templates/c/firmware-info/tools/make_build_id.ps1" -Repository "." -Output "generated/firmware_build_id.h"
|
||||
```
|
||||
|
||||
Указывайте `-Repository` и `-Output` явно: расположение сабмодуля и рабочий
|
||||
каталог IDE различаются между проектами. `-Repository` должен указывать на
|
||||
исходники прошивки, а не на репозиторий `templates`. Для Keil из каталога `mdk`
|
||||
соответственно используйте `..\lib\templates\...`, `-Repository ".."` и
|
||||
`-Output ".\Generated\firmware_build_id.h"`.
|
||||
|
||||
При доступном Git чистый checkout получает 8 знаков commit, изменения tracked
|
||||
файлов — 7 знаков и `+`. Если commit определить нельзя, используется `NOGIT000`.
|
||||
Новые untracked-файлы текущий генератор при определении dirty не учитывает.
|
||||
Дата/время берутся из `__DATE__`/`__TIME__` при компиляции `firmware_info_port.c`;
|
||||
для выпуска выполняйте полный Rebuild, чтобы не оставить старый объектный файл.
|
||||
|
||||
## 3. Обработчик запроса версии
|
||||
|
||||
Пример функции подготовки полезной нагрузки, вызываемой вашим обработчиком:
|
||||
|
||||
```c
|
||||
#include "firmware_info_port.h"
|
||||
|
||||
firmware_info_status_t app_make_firmware_info(uint8_t *payload, size_t capacity)
|
||||
{
|
||||
firmware_info_t info;
|
||||
firmware_info_status_t status = firmware_info_port_describe(&info);
|
||||
if (status != FIRMWARE_INFO_OK) return status;
|
||||
return firmware_info_to_le_bytes(&info, payload, capacity);
|
||||
}
|
||||
```
|
||||
|
||||
При успехе передайте ровно `FIRMWARE_INFO_PAYLOAD_SIZE` (24) байта в собственный
|
||||
транспорт. При ошибке верните ошибку протокола, а не содержимое буфера.
|
||||
Не отправляйте `sizeof(firmware_info_t)`: структура не является wire format.
|
||||
|
||||
В KONOR обработчик `app_send_firmware_info()` отвечает на
|
||||
`PROTO_MSG_FIRMWARE_INFO = 0x03`, сохраняя sequence запроса. Старый 32-байтовый
|
||||
`DEVICE_INFO` остаётся отдельным сообщением. Для нового протокола сначала
|
||||
согласуйте команду с клиентом: подключение библиотеки само по себе не добавит
|
||||
декодер и отображение версии в GUI.
|
||||
|
||||
Для Modbus вызовите `firmware_info_to_registers()` с массивом из 12 `uint16_t`
|
||||
и разместите его в выбранной карте регистров. Адреса и Modbus-порядок байтов
|
||||
обеспечивает ваш Modbus-стек; LE-буфер в Modbus напрямую не копируйте.
|
||||
|
||||
## 4. Контракт v1
|
||||
|
||||
| Индекс слова | Содержимое |
|
||||
|---|---|
|
||||
| 0 | Версия контракта: 1 |
|
||||
| 1, 2, 3 | major, minor, patch |
|
||||
| 4 | Год |
|
||||
| 5 | `(month << 8) \| day` |
|
||||
| 6 | `(hour << 8) \| minute` |
|
||||
| 7 | Секунды |
|
||||
| 8–11 | По два ASCII-символа build ID: первый в старшем байте слова |
|
||||
|
||||
В LE-представлении младший байт каждого слова идёт первым. Например, build ID
|
||||
`ab` в начале строки даёт слово `0x6162`, но байты `62 61`; декодируйте сначала
|
||||
слово, затем символы из старшего/младшего байта. NUL-терминатор не передаётся.
|
||||
|
||||
Текущая проверка допускает major/minor до 255 и patch до 999. Если в каталоге
|
||||
используется `(major << 16) | (minor << 8) | patch`, ограничьте **все три** части
|
||||
до 255, иначе значения пересекутся. Версия каталога автоматически из C-config
|
||||
не извлекается.
|
||||
|
||||
Для C2000 с 16-битным `char` нельзя считать готовым байтовый порт: отдельно
|
||||
проверьте наличие `uint8_t` и представление октетов в транспорте. Публикация
|
||||
TMS SCI8-файла с ПК поддерживается независимо от переноса этой C-библиотеки.
|
||||
|
||||
## 5. Проверка переноса
|
||||
|
||||
1. Соберите host-тест из `tests/test_firmware_info.c` вместе с ядром либо
|
||||
используйте CMake/CTest. Он проверяет дату, регистры и порядок байтов build ID.
|
||||
2. Соберите целевую прошивку с обоими `.c` и сгенерированным заголовком.
|
||||
3. Запросите версию у устройства: сравните SemVer, build ID, время и 24-байтовый
|
||||
ответ с конкретной сборкой. Проверьте, что прежний `DEVICE_INFO` не изменился.
|
||||
4. Подключите выпуск по инструкции публикатора; сравните версию в config и
|
||||
`firmware-release.cmd` перед Rebuild и `--preflight`.
|
||||
|
||||
@@ -25,3 +25,7 @@ config, а не дублирования ядра.
|
||||
|
||||
KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый
|
||||
32-байтовый `DEVICE_INFO` без изменений.
|
||||
|
||||
Подробная инструкция с кодом обработчика, форматом ответа и проверкой переноса:
|
||||
[`PORTING.md`](PORTING.md). Для размещения файла прошивки в каталоге SETGUI
|
||||
используется отдельный [общий публикатор](../../tools/firmware-publish/PORTING.md).
|
||||
|
||||
93
c/set-protocol/ALTERA_LOGIC.md
Normal file
93
c/set-protocol/ALTERA_LOGIC.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Altera Logic: общее ядро и порты
|
||||
|
||||
Потоковый SETCAN/GAS-профиль и общие порты описаны отдельно:
|
||||
[Altera Logic ONLINE](../../doc/setcan/ALTERA_LOGIC_STREAM.md).
|
||||
Ниже — совместимый прежний буферный протокол UART `A5 CMD`.
|
||||
|
||||
Клиент UART-протокола FPGA-анализатора Cyclone IV: 16 каналов, 4096 выборок.
|
||||
Работает с существующим RTL `altera_loganalisator`, версия протокола 1.
|
||||
Ядро C99 не зависит от Qt, ОС, системных часов и динамической памяти.
|
||||
|
||||
Слои: SETGUI → Python ctypes → C99 → Python Qt-порт → COM / USB–UART.
|
||||
GUI передаёт числовые настройки и получает готовый массив; формат пакетов,
|
||||
XOR, endian, последовательность команд, парсер и состояния тайм-аутов находятся в C.
|
||||
|
||||
| Файл | Назначение и зависимости |
|
||||
|---|---|
|
||||
| `include/altera_logic.h` | Публичный ABI, макрос экспорта из `pcan_abi.h` |
|
||||
| `src/altera_logic.c` | Кодек и автомат обмена; только стандартная библиотека C |
|
||||
| `tests/test_altera_logic.c` | Эталонные пакеты RTL, полный захват, ошибки |
|
||||
| `../../python/altera_logic/native.py` | ctypes-модели, загрузка DLL, экспорт CSV; stdlib |
|
||||
| `../../python/altera_logic/qt_port.py` | Асинхронный COM-порт; PySide6 QtSerialPort |
|
||||
|
||||
## Сборка
|
||||
|
||||
Из корня `templates`:
|
||||
|
||||
```powershell
|
||||
python c/set-protocol/tools/build_host.py --output python/altera_logic/native/setprotocol.dll
|
||||
```
|
||||
|
||||
На Linux выходной файл — `python/altera_logic/native/libsetprotocol.so`.
|
||||
Также поддерживается основная CMake-сборка `c/set-protocol`; путь к результату
|
||||
задаётся через `ALTERA_LOGIC_LIBRARY`. Библиотека без символов `la_*`
|
||||
не поддерживается: тихого Python-fallback нет.
|
||||
|
||||
## Контракт порта
|
||||
|
||||
Выделить выровненную память размером `la_context_size()`; все обращения к
|
||||
одному контексту выполняются в одном потоке.
|
||||
|
||||
- `la_init(ctx)` — начать с INFO.
|
||||
- `la_next(ctx, out, capacity)` — получить следующий запрос, либо 0, если отправлять нечего.
|
||||
- `la_feed(ctx, bytes, size)` — передать принятые байты, включая частичные ответы.
|
||||
- `la_tick(ctx, elapsed_ms)` — сообщить прошедшее время; тайм-аут ответа 1000 мс.
|
||||
- `la_start(ctx, divider, mask, value, edge_mask, edge_value)` — настройка и новый захват.
|
||||
- `la_get(ctx, field)` — состояние и прогресс; `la_samples` — завершённая запись.
|
||||
- `la_fail(ctx, LA_IO_ERROR)` — сообщить ошибку транспорта.
|
||||
|
||||
COM: 921600 бод, 8N1, без управления потоком. Одновременно ожидается только
|
||||
один ответ. После ARM STATUS опрашивается каждые 25 мс; ожидание самого
|
||||
триггера не ограничено. После done читаются 64 блока по 64 выборки.
|
||||
Ошибки закрывают сеанс без автоматического повтора ARM. Неверный размер,
|
||||
код команды или XOR прекращает сеанс. При рассинхронизации запроса на FPGA
|
||||
может потребоваться аппаратный RESET_N; переподключение не гарантирует её сброс.
|
||||
|
||||
Клиент намеренно принимает только профиль 16 × 4096 версии 1.
|
||||
Поддержка внешней SDRAM и расширенных адресов сюда пока не входит.
|
||||
Прерывание в GUI закрывает порт ПК: STOP в протоколе FPGA отсутствует.
|
||||
|
||||
## Быстрый старт без платы
|
||||
|
||||
Добавить `templates/python` в PYTHONPATH:
|
||||
|
||||
```python
|
||||
from altera_logic import NativeAnalyzer
|
||||
|
||||
client = NativeAnalyzer()
|
||||
client.reset(demo=True)
|
||||
client.start(divider=49)
|
||||
capture = client.capture()
|
||||
print(len(capture.samples)) # 4096
|
||||
print(capture.sample_rate) # 1_000_000
|
||||
print(capture.trigger_index) # 2048
|
||||
capture.save_csv("demo.csv")
|
||||
```
|
||||
|
||||
Демо создаёт синтетическую запись в C; не моделирует заданные условия триггера.
|
||||
CSV содержит явный признак demo, время относительно триггера и состояния D0…D15.
|
||||
|
||||
## Потребители и проверка
|
||||
|
||||
Используется вкладкой SETGUI «Altera Logic». Для Android/JNI новый потребитель
|
||||
может использовать тот же ABI; интерфейс Android в этой задаче не изменяется.
|
||||
Существующий ABI SETProtocol не изменён, добавлены только символы `la_*`.
|
||||
|
||||
```powershell
|
||||
$env:PYTHONPATH = "$PWD/python"
|
||||
python -m unittest discover -s python/tests -p test_altera_logic.py
|
||||
```
|
||||
|
||||
CMake включает `test_altera_logic` и CTest `altera_logic`.
|
||||
На плате требуется отдельно проверить разводку пинов, RAM-модуль и прошивку;
|
||||
программные проверки не заменяют аппаратную проверку.
|
||||
@@ -6,6 +6,8 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
|
||||
set(SETPROTOCOL_V2_SOURCES
|
||||
src/altera_logic.c
|
||||
src/altera_stream.c
|
||||
src/set_protocol.c
|
||||
src/set_can.c
|
||||
src/set_firmware.c
|
||||
@@ -79,6 +81,12 @@ endif()
|
||||
option(SETP_BUILD_TESTS "Build host tests" ON)
|
||||
if(SETP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(test_altera_logic tests/test_altera_logic.c)
|
||||
target_link_libraries(test_altera_logic PRIVATE setprotocol_static)
|
||||
add_test(NAME altera_logic COMMAND test_altera_logic)
|
||||
add_executable(test_altera_stream tests/test_altera_stream.c)
|
||||
target_link_libraries(test_altera_stream PRIVATE setprotocol_static)
|
||||
add_test(NAME altera_stream COMMAND test_altera_stream)
|
||||
|
||||
add_executable(test_plot tests/test_plot.c)
|
||||
target_link_libraries(test_plot PRIVATE setprotocol_static)
|
||||
|
||||
35
c/set-protocol/include/altera_logic.h
Normal file
35
c/set-protocol/include/altera_logic.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/** Altera logic analyzer UART client. C99, caller-owned memory, no OS/heap.
|
||||
* One request at a time; no automatic retries (ARM is not idempotent).
|
||||
* Initialize aligned storage of la_context_size() bytes, then call la_next,
|
||||
* la_feed and la_tick from one thread. tick receives elapsed milliseconds.
|
||||
*/
|
||||
#ifndef ALTERA_LOGIC_H
|
||||
#define ALTERA_LOGIC_H
|
||||
#include "pcan_abi.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
enum { LA_CONNECTING, LA_READY, LA_CAPTURING, LA_DONE, LA_ERROR };
|
||||
enum { LA_OK, LA_BAD_FRAME, LA_CHECKSUM, LA_DEVICE_ERROR, LA_TIMEOUT,
|
||||
LA_UNSUPPORTED, LA_ARGUMENT, LA_IO_ERROR };
|
||||
enum { LA_STATE, LA_ERROR_CODE, LA_CHANNELS, LA_DEPTH, LA_CLOCK_HZ,
|
||||
LA_FLAGS, LA_TRIGGER_INDEX, LA_READ_COUNT, LA_DIVIDER };
|
||||
PCAN_ABI_API size_t la_context_size(void);
|
||||
PCAN_ABI_API void la_init(void *ctx);
|
||||
/** Return 0 on success. Configuration is immutable throughout this capture. */
|
||||
PCAN_ABI_API int la_start(void *ctx, uint32_t divider, uint32_t mask,
|
||||
uint32_t value, uint32_t edge_mask, uint32_t edge_value);
|
||||
/** Returns 6 when a request is ready, otherwise 0. Capacity must be >=6. */
|
||||
PCAN_ABI_API size_t la_next(void *ctx, uint8_t *out, size_t capacity);
|
||||
PCAN_ABI_API void la_feed(void *ctx, const uint8_t *data, size_t size);
|
||||
PCAN_ABI_API void la_tick(void *ctx, uint32_t elapsed_ms);
|
||||
PCAN_ABI_API void la_fail(void *ctx, uint32_t code);
|
||||
PCAN_ABI_API uint32_t la_get(const void *ctx, uint32_t field);
|
||||
PCAN_ABI_API size_t la_samples(const void *ctx, uint16_t *out, size_t capacity);
|
||||
/** Offline fixture, never communicates with hardware or claims a real trigger. */
|
||||
PCAN_ABI_API void la_demo_init(void *ctx);
|
||||
PCAN_ABI_API int la_demo_capture(void *ctx, uint32_t divider);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
46
c/set-protocol/include/altera_stream.h
Normal file
46
c/set-protocol/include/altera_stream.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/** SETCAN/GAS Altera Logic online stream, shared by CAN and UART.
|
||||
* Caller-owned context; one serialized caller. No heap, OS or Qt dependencies.
|
||||
*/
|
||||
#ifndef ALTERA_STREAM_H
|
||||
#define ALTERA_STREAM_H
|
||||
#include "pcan_abi.h"
|
||||
#include "pcan_frame.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#define LAS_DEVICE_TYPE 6U
|
||||
#define LAS_DEVICE_ID 14U
|
||||
#define LAS_GAS_META 0xFE00U
|
||||
#define LAS_GAS_DATA 0xFF00U
|
||||
#define LAS_CAPACITY 8192U
|
||||
#define LAS_TAG 0x4C01U
|
||||
enum { LAS_COUNT, LAS_PERIOD_NS, LAS_SESSION, LAS_RECEIVED, LAS_MISSING,
|
||||
LAS_DUPLICATES, LAS_INVALID, LAS_IGNORED, LAS_CRC_ERRORS, LAS_HAS_META };
|
||||
PCAN_ABI_API size_t las_context_size(void);
|
||||
PCAN_ABI_API int las_init(void *ctx, uint32_t device_id);
|
||||
PCAN_ABI_API void las_can(void *ctx, uint32_t id, const uint8_t *data, size_t size,
|
||||
uint32_t extended, uint32_t remote);
|
||||
PCAN_ABI_API void las_uart(void *ctx, const uint8_t *data, size_t size);
|
||||
PCAN_ABI_API uint32_t las_get(const void *ctx, uint32_t field);
|
||||
/** Chronological bounded history. breaks[i]=1 marks a discontinuity before i. */
|
||||
PCAN_ABI_API size_t las_snapshot(const void *ctx, uint64_t *indices, uint16_t *samples,
|
||||
uint8_t *breaks, size_t capacity);
|
||||
/** Sparse step trace for rendering; capacity >= 2*LAS_COUNT, move=pen-up. */
|
||||
PCAN_ABI_API size_t las_trace(const void *ctx, uint32_t channel, uint64_t *indices,
|
||||
uint8_t *levels, uint8_t *moves, size_t capacity);
|
||||
PCAN_ABI_API uint32_t las_device_type(void);
|
||||
PCAN_ABI_API uint32_t las_device_id(void);
|
||||
PCAN_ABI_API const char *las_device_name(void);
|
||||
PCAN_ABI_API void las_demo_step(void *ctx, uint32_t count);
|
||||
/** Device-side packet builders; return zero for invalid arguments.
|
||||
* uart=0: 8-byte CAN data + *id; uart=1: complete AA55 transport + *id.
|
||||
* Same builders are used by firmware ports, tests and the demo producer.
|
||||
*/
|
||||
PCAN_ABI_API size_t las_metadata(uint32_t device, uint32_t session, uint32_t period_ns,
|
||||
uint32_t uart, uint32_t *id, uint8_t *out, size_t capacity);
|
||||
PCAN_ABI_API size_t las_data(uint32_t device, uint32_t session, uint32_t index,
|
||||
uint32_t sample, uint32_t uart, uint32_t *id, uint8_t *out, size_t capacity);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
142
c/set-protocol/src/altera_logic.c
Normal file
142
c/set-protocol/src/altera_logic.c
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "altera_logic.h"
|
||||
#include <string.h>
|
||||
typedef struct {
|
||||
uint32_t state, error, clock_hz, elapsed, delay;
|
||||
uint16_t config[5], channels, depth, trigger, count, offset;
|
||||
uint16_t samples[4096];
|
||||
uint8_t command, pending, flags, requested, rx[133];
|
||||
size_t used;
|
||||
} la_context;
|
||||
static uint8_t checksum(const uint8_t *p, size_t n) {
|
||||
uint8_t x = 0; size_t i;
|
||||
for (i = 0; i < n; ++i) x ^= p[i];
|
||||
return x;
|
||||
}
|
||||
static uint16_t u16(const uint8_t *p) {
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
size_t la_context_size(void) { return sizeof(la_context); }
|
||||
void la_init(void *ctx) {
|
||||
la_context *s = (la_context *)ctx;
|
||||
memset(s, 0, sizeof(*s)); s->command = 1; s->state = LA_CONNECTING;
|
||||
}
|
||||
void la_fail(void *ctx, uint32_t code) {
|
||||
la_context *s = (la_context *)ctx;
|
||||
s->state = LA_ERROR; s->error = code; s->pending = 0; s->command = 0;
|
||||
}
|
||||
int la_start(void *ctx, uint32_t div, uint32_t mask, uint32_t val,
|
||||
uint32_t emask, uint32_t eval) {
|
||||
la_context *s = (la_context *)ctx;
|
||||
if ((s->state != LA_READY && s->state != LA_DONE) ||
|
||||
div > 65535 || mask > 65535 || val > 65535 ||
|
||||
emask > 65535 || eval > 65535 || ((val ^ eval) & mask & emask))
|
||||
return LA_ARGUMENT;
|
||||
s->config[0]=(uint16_t)div; s->config[1]=(uint16_t)mask;
|
||||
s->config[2]=(uint16_t)val; s->config[3]=(uint16_t)emask;
|
||||
s->config[4]=(uint16_t)eval;
|
||||
s->state=LA_CAPTURING; s->error=0; s->flags=0; s->count=0;
|
||||
s->offset=0; s->command=2; s->delay=0;
|
||||
return LA_OK;
|
||||
}
|
||||
size_t la_next(void *ctx, uint8_t *out, size_t capacity) {
|
||||
la_context *s = (la_context *)ctx; uint16_t arg = 0;
|
||||
if (!out || capacity < 6 || !s->command || s->pending || s->delay) return 0;
|
||||
if (s->command >= 2 && s->command <= 6) arg=s->config[s->command-2];
|
||||
if (s->command == 9) {
|
||||
arg=s->offset;
|
||||
s->requested=(uint8_t)((s->depth-s->offset > 64) ? 64 : s->depth-s->offset);
|
||||
}
|
||||
out[0]=0xa5; out[1]=s->command; out[2]=(uint8_t)arg;
|
||||
out[3]=(uint8_t)(arg >> 8); out[4]=(s->command == 9) ? s->requested : 0;
|
||||
out[5]=checksum(out,5);
|
||||
s->pending=1; s->elapsed=0; s->used=0;
|
||||
return 6;
|
||||
}
|
||||
static void complete(la_context *s) {
|
||||
size_t i;
|
||||
if (checksum(s->rx,s->used)) { la_fail(s,LA_CHECKSUM); return; }
|
||||
if (s->rx[2]) { la_fail(s,LA_DEVICE_ERROR); return; }
|
||||
s->pending=0;
|
||||
if (s->command == 1) {
|
||||
s->channels=s->rx[3]; s->depth=u16(s->rx+4);
|
||||
s->clock_hz=(uint32_t)s->rx[6]*1000000U;
|
||||
if (s->channels != 16 || s->depth != 4096 || !s->clock_hz || s->rx[7]!=1) {
|
||||
la_fail(s,LA_UNSUPPORTED); return;
|
||||
}
|
||||
s->state=LA_READY; s->command=0;
|
||||
} else if (s->command < 7) ++s->command;
|
||||
else if (s->command == 7) { s->command=8; s->delay=25; }
|
||||
else if (s->command == 8) {
|
||||
s->flags=s->rx[3]; s->trigger=u16(s->rx+4);
|
||||
if (s->flags & 4) {
|
||||
if (s->trigger >= s->depth || (s->flags & 1)) { la_fail(s,LA_BAD_FRAME); return; }
|
||||
s->command=9;
|
||||
} else s->delay=25;
|
||||
} else {
|
||||
for(i=0; i<s->requested; ++i) s->samples[s->offset+i]=u16(s->rx+4+i*2);
|
||||
s->offset=(uint16_t)(s->offset+s->requested); s->count=s->offset;
|
||||
if(s->offset == s->depth) { s->state=LA_DONE; s->command=0; }
|
||||
}
|
||||
}
|
||||
void la_feed(void *ctx, const uint8_t *data, size_t size) {
|
||||
la_context *s = (la_context *)ctx; size_t i, expected;
|
||||
if (!data) return;
|
||||
for(i=0; i<size; ++i) {
|
||||
if(!s->pending || s->used >= sizeof(s->rx)) { la_fail(s,LA_BAD_FRAME); return; }
|
||||
s->rx[s->used++]=data[i];
|
||||
if(s->rx[0]!=0x5a || (s->used>=2 && s->rx[1]!=(s->command|0x80))) {
|
||||
la_fail(s,LA_BAD_FRAME); return;
|
||||
}
|
||||
if(s->used<3) continue;
|
||||
expected=4;
|
||||
if(!s->rx[2]) {
|
||||
if(s->command==1) expected=9;
|
||||
else if(s->command==8) expected=7;
|
||||
else if(s->command==9) {
|
||||
if(s->used<4) continue;
|
||||
if(s->rx[3]!=s->requested) { la_fail(s,LA_BAD_FRAME); return; }
|
||||
expected=5U+2U*s->requested;
|
||||
}
|
||||
}
|
||||
if(s->used==expected) complete(s);
|
||||
}
|
||||
}
|
||||
void la_tick(void *ctx, uint32_t ms) {
|
||||
la_context *s=(la_context *)ctx;
|
||||
if(s->pending) {
|
||||
if(ms >= 1000U-s->elapsed) la_fail(s,LA_TIMEOUT);
|
||||
else s->elapsed+=ms;
|
||||
}
|
||||
s->delay=(ms >= s->delay) ? 0 : s->delay-ms;
|
||||
}
|
||||
uint32_t la_get(const void *ctx, uint32_t field) {
|
||||
const la_context *s=(const la_context *)ctx;
|
||||
switch(field) {
|
||||
case LA_STATE:return s->state; case LA_ERROR_CODE:return s->error;
|
||||
case LA_CHANNELS:return s->channels; case LA_DEPTH:return s->depth;
|
||||
case LA_CLOCK_HZ:return s->clock_hz; case LA_FLAGS:return s->flags;
|
||||
case LA_TRIGGER_INDEX:return s->trigger; case LA_READ_COUNT:return s->count;
|
||||
case LA_DIVIDER:return s->config[0]; default:return 0;
|
||||
}
|
||||
}
|
||||
size_t la_samples(const void *ctx, uint16_t *out, size_t capacity) {
|
||||
const la_context *s=(const la_context *)ctx;
|
||||
if(s->state!=LA_DONE || !out || capacity<s->count) return 0;
|
||||
memcpy(out,s->samples,s->count*sizeof(uint16_t)); return s->count;
|
||||
}
|
||||
void la_demo_init(void *ctx) {
|
||||
la_context *s=(la_context *)ctx; la_init(s);
|
||||
s->channels=16; s->depth=4096; s->clock_hz=50000000;
|
||||
s->state=LA_READY; s->command=0;
|
||||
}
|
||||
int la_demo_capture(void *ctx, uint32_t divider) {
|
||||
la_context *s=(la_context *)ctx; size_t i, bit;
|
||||
int rc=la_start(s,divider,0,0,0,0); if(rc) return rc;
|
||||
for(i=0;i<4096;++i) {
|
||||
uint16_t v=0;
|
||||
for(bit=0;bit<16;++bit) if(((i/(4U+bit*7U))&1U)!=0) v|=(uint16_t)(1U<<bit);
|
||||
s->samples[i]=v;
|
||||
}
|
||||
s->trigger=2048; s->flags=6; s->count=4096; s->command=0; s->state=LA_DONE;
|
||||
return LA_OK;
|
||||
}
|
||||
158
c/set-protocol/src/altera_stream.c
Normal file
158
c/set-protocol/src/altera_stream.c
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "altera_stream.h"
|
||||
#include "pcan_id.h"
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
pcan_parser_t parser;
|
||||
uint64_t indices[LAS_CAPACITY], next_index;
|
||||
uint16_t samples[LAS_CAPACITY];
|
||||
uint8_t breaks[LAS_CAPACITY];
|
||||
uint32_t head, count, period, session, received, missing, duplicates, invalid, ignored;
|
||||
uint8_t device, metadata, started;
|
||||
} las_context;
|
||||
static uint16_t r16(const uint8_t *p) { return (uint16_t)(p[0]|((uint16_t)p[1]<<8)); }
|
||||
static uint32_t r32(const uint8_t *p) { return (uint32_t)r16(p)|((uint32_t)r16(p+2)<<16); }
|
||||
static void w16(uint8_t *p,uint16_t v) { p[0]=(uint8_t)v;p[1]=(uint8_t)(v>>8); }
|
||||
static void w32(uint8_t *p,uint32_t v) { w16(p,(uint16_t)v);w16(p+2,(uint16_t)(v>>16)); }
|
||||
uint32_t las_device_type(void) { return LAS_DEVICE_TYPE; }
|
||||
uint32_t las_device_id(void) { return LAS_DEVICE_ID; }
|
||||
const char *las_device_name(void) { return "Altera Logic"; }
|
||||
size_t las_context_size(void) { return sizeof(las_context); }
|
||||
int las_init(void *ctx,uint32_t device) {
|
||||
las_context *s=(las_context *)ctx;
|
||||
if(!ctx || device>15) return 0;
|
||||
memset(s,0,sizeof(*s));s->device=(uint8_t)device;pcan_parser_init(&s->parser);return 1;
|
||||
}
|
||||
static void append(las_context *s,uint64_t index,uint16_t value,uint8_t gap) {
|
||||
s->indices[s->head]=index;s->samples[s->head]=value;s->breaks[s->head]=gap;
|
||||
s->head=(s->head+1U)%LAS_CAPACITY;
|
||||
if(s->count<LAS_CAPACITY) ++s->count;
|
||||
}
|
||||
void las_can(void *ctx,uint32_t raw,const uint8_t *p,size_t n,uint32_t extended,uint32_t remote) {
|
||||
las_context *s=(las_context *)ctx;pcan_id_t id;uint32_t index,delta,period,session;
|
||||
if(!extended || remote || raw>PCAN_ID_MASK) { ++s->ignored;return; }
|
||||
pcan_id_unpack(raw,&id);
|
||||
if(id.device_type!=LAS_DEVICE_TYPE || id.device_id!=s->device ||
|
||||
id.route!=PCAN_ROUTE_FROM_DEVICE || id.msg_type!=PCAN_MSG_GAS ||
|
||||
(id.msg_body!=LAS_GAS_META && id.msg_body!=LAS_GAS_DATA)) { ++s->ignored;return; }
|
||||
if(n!=8 || !p) { ++s->invalid;return; }
|
||||
if(id.msg_body==LAS_GAS_META) {
|
||||
period=r32(p+4);session=r16(p+2);
|
||||
if(r16(p)!=LAS_TAG || period==0) { ++s->invalid;return; }
|
||||
if(s->metadata && session==s->session) {
|
||||
if(period!=s->period) ++s->invalid;
|
||||
return;
|
||||
}
|
||||
if(s->metadata && (uint16_t)(session-s->session)>=0x8000U) {
|
||||
++s->duplicates;return;
|
||||
}
|
||||
s->metadata=1;s->session=session;s->period=period;
|
||||
s->count=0;s->head=0;s->started=0;s->next_index=0;
|
||||
return;
|
||||
}
|
||||
if(!s->metadata) { ++s->ignored;return; }
|
||||
if(r16(p)!=s->session) { ++s->ignored;return; }
|
||||
index=r32(p+4);
|
||||
delta=0;
|
||||
if(s->started) {
|
||||
delta=index-(uint32_t)s->next_index;
|
||||
if(delta>=0x80000000U) { ++s->duplicates;return; }
|
||||
s->missing+=delta;
|
||||
} else s->next_index=index;
|
||||
s->next_index+=delta;
|
||||
append(s,s->next_index,r16(p+2),(uint8_t)(!s->started || delta!=0));
|
||||
s->next_index+=1;s->started=1;++s->received;
|
||||
}
|
||||
static void frame_callback(const pcan_frame_t *frame,void *user) {
|
||||
if(frame->flags & (PCAN_FLAG_DIR|PCAN_FLAG_ERR)) {
|
||||
++((las_context *)user)->ignored;return;
|
||||
}
|
||||
las_can(user,frame->id,frame->data,frame->dlc,
|
||||
frame->flags&PCAN_FLAG_IDE,frame->flags&PCAN_FLAG_RTR);
|
||||
}
|
||||
void las_uart(void *ctx,const uint8_t *data,size_t size) {
|
||||
las_context *s=(las_context *)ctx;
|
||||
if(data) pcan_parser_feed(&s->parser,data,size,frame_callback,s);
|
||||
}
|
||||
uint32_t las_get(const void *ctx,uint32_t field) {
|
||||
const las_context *s=(const las_context *)ctx;
|
||||
switch(field) {
|
||||
case LAS_COUNT:return s->count;case LAS_PERIOD_NS:return s->period;
|
||||
case LAS_SESSION:return s->session;case LAS_RECEIVED:return s->received;
|
||||
case LAS_MISSING:return s->missing;case LAS_DUPLICATES:return s->duplicates;
|
||||
case LAS_INVALID:return s->invalid;case LAS_IGNORED:return s->ignored;
|
||||
case LAS_CRC_ERRORS:return s->parser.stats.crc_errors;
|
||||
case LAS_HAS_META:return s->metadata;default:return 0;
|
||||
}
|
||||
}
|
||||
size_t las_snapshot(const void *ctx,uint64_t *indices,uint16_t *samples,uint8_t *breaks,size_t cap) {
|
||||
const las_context *s=(const las_context *)ctx;uint32_t i,pos;
|
||||
if(!indices || !samples || !breaks || cap<s->count) return 0;
|
||||
pos=(s->head+LAS_CAPACITY-s->count)%LAS_CAPACITY;
|
||||
for(i=0;i<s->count;++i) {
|
||||
indices[i]=s->indices[pos];samples[i]=s->samples[pos];breaks[i]=s->breaks[pos];
|
||||
pos=(pos+1U)%LAS_CAPACITY;
|
||||
}
|
||||
return s->count;
|
||||
}
|
||||
size_t las_trace(const void *ctx,uint32_t channel,uint64_t *indices,uint8_t *levels,
|
||||
uint8_t *moves,size_t cap) {
|
||||
const las_context *s=(const las_context *)ctx;uint32_t i,pos;
|
||||
uint64_t previous_x=0;uint8_t previous_y=0;size_t n=0;
|
||||
if(channel>=16 || !indices || !levels || !moves || cap<2U*s->count) return 0;
|
||||
pos=(s->head+LAS_CAPACITY-s->count)%LAS_CAPACITY;
|
||||
for(i=0;i<s->count;++i) {
|
||||
uint64_t x=s->indices[pos];uint8_t y=(uint8_t)((s->samples[pos]>>channel)&1U);
|
||||
if(!i) { indices[n]=x;levels[n]=y;moves[n++]=1; }
|
||||
else if(s->breaks[pos]) {
|
||||
indices[n]=previous_x;levels[n]=previous_y;moves[n++]=0;
|
||||
indices[n]=x;levels[n]=y;moves[n++]=1;
|
||||
} else if(y!=previous_y) {
|
||||
indices[n]=x;levels[n]=previous_y;moves[n++]=0;
|
||||
indices[n]=x;levels[n]=y;moves[n++]=0;
|
||||
}
|
||||
previous_x=x;previous_y=y;pos=(pos+1U)%LAS_CAPACITY;
|
||||
}
|
||||
if(s->count>1) { indices[n]=previous_x;levels[n]=previous_y;moves[n++]=0; }
|
||||
return n;
|
||||
}
|
||||
static size_t packet(uint32_t device,uint16_t body,const uint8_t *data,uint32_t uart,
|
||||
uint32_t *raw,uint8_t *out,size_t cap) {
|
||||
pcan_id_t id;pcan_frame_t frame;
|
||||
if(device>15 || !raw || !out || cap<(uart?PCAN_FRAME_MAX:8U)) return 0;
|
||||
memset(&id,0,sizeof(id));id.device_type=LAS_DEVICE_TYPE;id.device_id=(uint8_t)device;
|
||||
id.priority=PCAN_PRIORITY_STANDARD;id.route=PCAN_ROUTE_FROM_DEVICE;
|
||||
id.msg_type=PCAN_MSG_GAS;id.msg_body=body;
|
||||
memset(&frame,0,sizeof(frame));frame.id=pcan_id_pack(&id);
|
||||
frame.flags=PCAN_FLAG_IDE;frame.dlc=8;memcpy(frame.data,data,8);*raw=frame.id;
|
||||
if(uart) return pcan_frame_encode(&frame,out,cap);
|
||||
memcpy(out,data,8);return 8;
|
||||
}
|
||||
size_t las_metadata(uint32_t device,uint32_t session,uint32_t period,uint32_t uart,
|
||||
uint32_t *id,uint8_t *out,size_t cap) {
|
||||
uint8_t data[8];if(session>65535 || !period) return 0;
|
||||
w16(data,LAS_TAG);w16(data+2,(uint16_t)session);w32(data+4,period);
|
||||
return packet(device,LAS_GAS_META,data,uart,id,out,cap);
|
||||
}
|
||||
size_t las_data(uint32_t device,uint32_t session,uint32_t index,uint32_t sample,uint32_t uart,
|
||||
uint32_t *id,uint8_t *out,size_t cap) {
|
||||
uint8_t data[8];if(session>65535 || sample>65535) return 0;
|
||||
w16(data,(uint16_t)session);w16(data+2,(uint16_t)sample);w32(data+4,index);
|
||||
return packet(device,LAS_GAS_DATA,data,uart,id,out,cap);
|
||||
}
|
||||
void las_demo_step(void *ctx,uint32_t count) {
|
||||
las_context *s=(las_context *)ctx;uint32_t i,bit,idx,raw;uint8_t packet_data[PCAN_FRAME_MAX];size_t n;
|
||||
if(!s->metadata) {
|
||||
n=las_metadata(s->device,1,1000000,1,&raw,packet_data,sizeof(packet_data));
|
||||
las_uart(s,packet_data,n);
|
||||
}
|
||||
if(count>4096) count=4096;
|
||||
for(i=0;i<count;++i) {
|
||||
uint16_t a=0;idx=(uint32_t)s->next_index;
|
||||
for(bit=0;bit<16;++bit) {
|
||||
if((idx/(5U+bit*7U))&1U) a|=(uint16_t)(1U<<bit);
|
||||
}
|
||||
n=las_data(s->device,s->session,idx,a,1,&raw,packet_data,sizeof(packet_data));
|
||||
las_uart(s,packet_data,n);
|
||||
}
|
||||
}
|
||||
72
c/set-protocol/tests/test_altera_logic.c
Normal file
72
c/set-protocol/tests/test_altera_logic.c
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "altera_logic.h"
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
static void response(void *s, uint8_t *data, size_t n) {
|
||||
size_t i; uint8_t sum=0;
|
||||
for(i=0;i<n-1;++i) sum^=data[i]; data[n-1]=sum;
|
||||
/* Serial reads can split at every byte. */
|
||||
for(i=0;i<n;++i) la_feed(s,data+i,1);
|
||||
}
|
||||
static void connect_ok(void *s) {
|
||||
uint8_t p[6], info[]={0x5a,0x81,0,16,0,16,50,1,0xe8};
|
||||
const uint8_t request[]={0xa5,1,0,0,0,0xa4};
|
||||
la_init(s); assert(la_next(s,p,5)==0);
|
||||
assert(la_next(s,p,6)==6 && !memcmp(p,request,6));
|
||||
assert(la_next(s,p,6)==0);
|
||||
response(s,info,sizeof(info)); assert(la_get(s,LA_STATE)==LA_READY);
|
||||
}
|
||||
int main(void) {
|
||||
void *s=calloc(1,la_context_size()); uint8_t p[6], ack[4], status[7], block[133];
|
||||
uint16_t samples[4096]; unsigned int cmd, offset, i;
|
||||
assert(s); connect_ok(s);
|
||||
assert(la_start(s,65536,0,0,0,0)==LA_ARGUMENT);
|
||||
assert(la_start(s,49,1,0,1,1)==LA_ARGUMENT);
|
||||
assert(la_start(s,49,0,0,1,1)==0);
|
||||
for(cmd=2;cmd<=7;++cmd) {
|
||||
assert(la_next(s,p,6)==6 && p[1]==cmd);
|
||||
if(cmd==2) { const uint8_t v[]={0xa5,2,49,0,0,0x96}; assert(!memcmp(p,v,6)); }
|
||||
ack[0]=0x5a; ack[1]=(uint8_t)(cmd|0x80); ack[2]=0; response(s,ack,4);
|
||||
}
|
||||
assert(la_next(s,p,6)==0); la_tick(s,25);
|
||||
assert(la_next(s,p,6)==6 && p[1]==8);
|
||||
{ uint8_t waiting[]={0x5a,0x88,0,1,0,8,0}; response(s,waiting,7); }
|
||||
la_tick(s,60000); /* Waiting for a physical trigger is not a UART timeout. */
|
||||
assert(la_get(s,LA_STATE)==LA_CAPTURING);
|
||||
assert(la_next(s,p,6)==6 && p[1]==8);
|
||||
status[0]=0x5a; status[1]=0x88; status[2]=0; status[3]=6;
|
||||
status[4]=0;status[5]=8;response(s,status,7);
|
||||
for(offset=0;offset<4096;offset+=64) {
|
||||
assert(la_next(s,p,6)==6 && p[1]==9 && p[4]==64);
|
||||
assert((unsigned int)(p[2]|(p[3]<<8))==offset);
|
||||
block[0]=0x5a;block[1]=0x89;block[2]=0;block[3]=64;
|
||||
for(i=0;i<64;++i) { block[4+2*i]=(uint8_t)(offset+i);block[5+2*i]=(uint8_t)((offset+i)>>8); }
|
||||
response(s,block,133);
|
||||
}
|
||||
assert(la_get(s,LA_STATE)==LA_DONE);
|
||||
assert(la_samples(s,samples,4095)==0);
|
||||
assert(la_samples(s,samples,4096)==4096);
|
||||
for(i=0;i<4096;++i) assert(samples[i]==i);
|
||||
assert(la_get(s,LA_TRIGGER_INDEX)==2048);
|
||||
/* Corrupt XOR, wrong command, device error, timeout and extra data. */
|
||||
la_init(s);la_next(s,p,6);
|
||||
{ uint8_t bad[]={0x5a,0x81,0,16,0,16,50,1,0};la_feed(s,bad,9); }
|
||||
assert(la_get(s,LA_ERROR_CODE)==LA_CHECKSUM);
|
||||
la_init(s);la_next(s,p,6);
|
||||
{ uint8_t bad[]={0x5a,0x88};la_feed(s,bad,2); }
|
||||
assert(la_get(s,LA_ERROR_CODE)==LA_BAD_FRAME);
|
||||
la_init(s);la_next(s,p,6);
|
||||
{ uint8_t bad[]={0x5a,0x81,1,0};response(s,bad,4); }
|
||||
assert(la_get(s,LA_ERROR_CODE)==LA_DEVICE_ERROR);
|
||||
la_init(s);la_next(s,p,6);la_tick(s,999);
|
||||
assert(la_get(s,LA_STATE)==LA_CONNECTING);la_tick(s,1);
|
||||
assert(la_get(s,LA_ERROR_CODE)==LA_TIMEOUT && la_next(s,p,6)==0);
|
||||
connect_ok(s);la_feed(s,p,1);assert(la_get(s,LA_ERROR_CODE)==LA_BAD_FRAME);
|
||||
la_init(s);la_next(s,p,6);
|
||||
{ uint8_t bad[]={0x5a,0x81,0,8,0,16,50,1,0};response(s,bad,9); }
|
||||
assert(la_get(s,LA_ERROR_CODE)==LA_UNSUPPORTED);
|
||||
la_demo_init(s);assert(la_demo_capture(s,49)==0);
|
||||
assert(la_get(s,LA_STATE)==LA_DONE && la_samples(s,samples,4096)==4096);
|
||||
free(s);puts("Altera Logic C tests passed");return 0;
|
||||
}
|
||||
62
c/set-protocol/tests/test_altera_stream.c
Normal file
62
c/set-protocol/tests/test_altera_stream.c
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "altera_stream.h"
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
static void meta(void *s,uint32_t session,int uart) {
|
||||
uint8_t data[32];uint32_t id;size_t n,i;
|
||||
n=las_metadata(LAS_DEVICE_ID,session,1000000,(uint32_t)uart,&id,data,sizeof(data));assert(n);
|
||||
assert(id==0x1EE3FE00U);
|
||||
if(uart) for(i=0;i<n;++i) las_uart(s,data+i,1);
|
||||
else las_can(s,id,data,n,1,0);
|
||||
}
|
||||
static void sample(void *s,uint32_t session,uint32_t index,int uart) {
|
||||
uint8_t data[32];uint32_t id;size_t n;
|
||||
n=las_data(LAS_DEVICE_ID,session,index,index&65535U,(uint32_t)uart,&id,data,sizeof(data));assert(n);
|
||||
assert(id==0x1EE3FF00U);
|
||||
if(uart) las_uart(s,data,n);else las_can(s,id,data,n,1,0);
|
||||
}
|
||||
int main(void) {
|
||||
void *can=malloc(las_context_size()),*uart=malloc(las_context_size());
|
||||
uint64_t *indices=malloc(LAS_CAPACITY*sizeof(uint64_t));
|
||||
uint16_t *samples=malloc(LAS_CAPACITY*sizeof(uint16_t));
|
||||
uint8_t *breaks=malloc(LAS_CAPACITY);uint8_t packet[32];uint32_t id;size_t n,i;
|
||||
assert(can&&uart&&indices&&samples&&breaks);
|
||||
assert(las_init(can,14)&&las_init(uart,14));assert(!las_init(can,16));
|
||||
sample(can,1,0,0);assert(las_get(can,LAS_COUNT)==0);
|
||||
meta(can,1,0);meta(uart,1,1);
|
||||
/* Independent golden SETCAN bytes. */
|
||||
n=las_data(14,1,0x12345678U,0x8001,0,&id,packet,sizeof(packet));
|
||||
{ const uint8_t v[]={1,0,1,0x80,0x78,0x56,0x34,0x12};assert(n==8&&!memcmp(v,packet,8)); }
|
||||
for(i=0;i<9000;++i) { sample(can,1,(uint32_t)i,0);sample(uart,1,(uint32_t)i,1); }
|
||||
assert(las_get(can,LAS_COUNT)==8192&&las_get(uart,LAS_COUNT)==8192);
|
||||
assert(las_get(can,LAS_RECEIVED)==9000&&las_get(uart,LAS_RECEIVED)==9000);
|
||||
assert(las_snapshot(can,indices,samples,breaks,LAS_CAPACITY)==8192);
|
||||
assert(indices[0]==808&&indices[8191]==8999&&samples[8191]==8999);
|
||||
assert(las_snapshot(uart,indices,samples,breaks,LAS_CAPACITY)==8192);
|
||||
assert(indices[0]==808&&indices[8191]==8999&&samples[8191]==8999);
|
||||
sample(can,1,8999,1);assert(las_get(can,LAS_DUPLICATES)==1); /* second transport duplicate */
|
||||
sample(can,1,9003,0);assert(las_get(can,LAS_MISSING)==3);
|
||||
las_snapshot(can,indices,samples,breaks,LAS_CAPACITY);assert(breaks[8191]);
|
||||
/* Repeated metadata must not erase rolling history. */
|
||||
meta(can,1,1);assert(las_get(can,LAS_COUNT)==8192);
|
||||
meta(can,2,0);assert(las_get(can,LAS_COUNT)==0);
|
||||
sample(can,1,9004,1);assert(las_get(can,LAS_COUNT)==0);
|
||||
meta(can,1,1);assert(las_get(can,LAS_SESSION)==2);
|
||||
sample(can,2,0xffffffffU,0);sample(can,2,0,1);
|
||||
las_snapshot(can,indices,samples,breaks,LAS_CAPACITY);
|
||||
assert(indices[0]==0xffffffffULL&&indices[1]==0x100000000ULL&&!breaks[1]);
|
||||
/* Reject wrong address, standard ID, RTR and short payload. */
|
||||
n=las_data(14,2,1,0,0,&id,packet,sizeof(packet));assert(n==8);
|
||||
las_can(can,id,packet,8,0,0);las_can(can,id,packet,8,1,1);
|
||||
las_can(can,id^(1U<<20),packet,8,1,0);las_can(can,id,packet,7,1,0);
|
||||
assert(las_get(can,LAS_COUNT)==2&&las_get(can,LAS_INVALID)==1);
|
||||
n=las_data(14,2,1,1,1,&id,packet,sizeof(packet));packet[n-1]^=0x80;
|
||||
las_uart(can,packet,n);assert(las_get(can,LAS_CRC_ERRORS)==1);
|
||||
sample(can,2,1,1);assert(las_get(can,LAS_COUNT)==3); /* recovery after CRC */
|
||||
assert(!las_metadata(14,65536,1000,0,&id,packet,sizeof(packet)));
|
||||
assert(!las_metadata(14,1,0,0,&id,packet,sizeof(packet)));
|
||||
assert(!las_data(14,1,0,65536,0,&id,packet,sizeof(packet)));
|
||||
free(can);free(uart);free(indices);free(samples);free(breaks);
|
||||
puts("Altera SETCAN/CAN/UART stream tests passed");return 0;
|
||||
}
|
||||
@@ -20,6 +20,8 @@ INCLUDE = ROOT / "include"
|
||||
JNI_INCLUDES: list[Path] = []
|
||||
SOURCES = [
|
||||
ROOT / "src" / name for name in (
|
||||
"altera_logic.c",
|
||||
"altera_stream.c",
|
||||
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
|
||||
"balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
||||
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
|
||||
|
||||
189
doc/setcan/ALTERA_LOGIC_STREAM.md
Normal file
189
doc/setcan/ALTERA_LOGIC_STREAM.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Altera Logic — ONLINE через SETCAN / GAS / UART
|
||||
|
||||
Версия профиля: **1**. Реализация, порты и тесты принадлежат `templates`.
|
||||
Это профиль существующего classic SETCAN/ProtoCAN с 29-битным ID;
|
||||
он не подменяется кадром SETP v2 и не использует старые команды `A5 CMD` FPGA.
|
||||
|
||||
## Адрес и носители
|
||||
|
||||
| Поле | Значение |
|
||||
|---|---|
|
||||
| Priority | 1, стандартный |
|
||||
| Route | 1, от устройства |
|
||||
| DeviceType | `0x6` |
|
||||
| DeviceID | `0xE` |
|
||||
| Device Name | `Altera Logic` |
|
||||
| MsgType | `0x3`, GAS |
|
||||
| CAN | Extended ID, data frame, DLC=8 |
|
||||
|
||||
CAN доставляет ID + 8 байт непосредственно. UART доставляет ровно тот же ID
|
||||
и те же данные в существующем контейнере `pcan_frame`:
|
||||
|
||||
```text
|
||||
AA 55 | LEN | SEQ | FLAGS | CAN_ID u32 LE | DATA | CRC16 u16 LE
|
||||
```
|
||||
|
||||
Для этого профиля LEN=14, FLAGS=1 (IDE), DIR/RTR/ERR сброшены,
|
||||
итоговая длина 19 байтов. CRC-16/CCITT-FALSE считается существующим кодеком
|
||||
по LEN…DATA. SEQ — транспортный 8-битный счётчик; порядок выборок определяется
|
||||
полем SampleIndex, поэтому независимые CAN/UART потоки дают одинаковый результат.
|
||||
Предоставленные простые C-билдеры устанавливают SEQ=0; устройство может вести
|
||||
его самостоятельно, используя `pcan_frame_encode`.
|
||||
Настройки прямого UART-порта GUI: **921600, 8N1, без flow control**.
|
||||
|
||||
## GAS: метаданные потока
|
||||
|
||||
**CAN ID `0x1EE3FE00`, MsgBody/GAS address `0xFE00`, DLC=8.**
|
||||
|
||||
| Регистры GAS | Байты DATA | Поле |
|
||||
|---|---|---|
|
||||
| FE00 | 0…1 | Tag/version = `0x4C01` |
|
||||
| FE01 | 2…3 | SessionID u16 |
|
||||
| FE02…FE03 | 4…7 | SamplePeriodNs u32, строго больше 0 |
|
||||
|
||||
Порядок байтов little-endian. Число каналов профиля фиксировано: 16.
|
||||
Частота выборок = `1e9 / SamplePeriodNs`.
|
||||
|
||||
Пример сессии 1 с частотой 1000 выборок/с:
|
||||
|
||||
```text
|
||||
ID: 1EE3FE00
|
||||
DATA: 01 4C 01 00 40 42 0F 00
|
||||
```
|
||||
|
||||
Источник отправляет метаданные перед первой выборкой и повторяет примерно
|
||||
раз в секунду, чтобы подключившийся GUI мог присоединиться к потоку.
|
||||
Повтор с теми же SessionID/периодом историю не очищает.
|
||||
|
||||
При перезапуске потока или изменении периода источник увеличивает SessionID
|
||||
по модулю 65536. Новый SessionID очищает историю; задержанные метаданные
|
||||
предыдущей сессии игнорируются. Разница менее 32768 считается движением вперёд.
|
||||
Если после аппаратного сброса устройство начало счётчик с меньшего значения,
|
||||
нужно остановить и снова включить приём в GUI (либо сохранять счётчик сессий
|
||||
на устройстве). Без полученных метаданных отсчёты не отображаются.
|
||||
|
||||
## GAS: одна выборка всех 16 каналов
|
||||
|
||||
**CAN ID `0x1EE3FF00`, MsgBody/GAS address `0xFF00`, DLC=8.**
|
||||
|
||||
| Регистры GAS | Байты DATA | Поле |
|
||||
|---|---|---|
|
||||
| FF00 | 0…1 | SessionID u16, должен совпадать с метаданными |
|
||||
| FF01 | 2…3 | Sample u16: бит n = уровень канала Dn |
|
||||
| FF02…FF03 | 4…7 | SampleIndex u32 |
|
||||
|
||||
Пример: сессия 1, выборка 0, D0 и D15 равны 1:
|
||||
|
||||
```text
|
||||
ID: 1EE3FF00
|
||||
DATA: 01 00 01 80 00 00 00 00
|
||||
```
|
||||
|
||||
SampleIndex увеличивается на каждой аппаратной выборке, в том числе если
|
||||
пакет не удалось передать. GUI замечает скачок индекса, учитывает потерянные
|
||||
выборки и рисует разрыв, не соединяя его выдуманными состояниями.
|
||||
Время получается из индекса и периода устройства, а не из времени прихода UART.
|
||||
|
||||
Переполнение индекса u32 разворачивается в u64 на приёмнике. Однозначное
|
||||
восстановление порядка требует разрыва меньше 2^31 выборок. При более долгом
|
||||
перерыве следует начать новую сессию. При подключении после полного оборота
|
||||
индекса GUI не знает число оборотов до первой принятой выборки.
|
||||
|
||||
Одновременный CAN+UART приём объединяет один поток: одинаковые или запоздалые
|
||||
индексы в одной сессии не добавляются повторно. Их число показывается как
|
||||
«дубли/старые». Поздние данные старой сессии игнорируются по SessionID.
|
||||
Если два источника прислали разные значения с одинаковыми SessionID/индексом,
|
||||
остаётся первый принятый пакет: это не механизм голосования или сверки каналов.
|
||||
|
||||
## Нагрузка и история
|
||||
|
||||
Профиль передаёт одну 16-битную выборку на CAN-кадр. Это осознанный обмен
|
||||
пропускной способности на независимую проверку сессии, потерь и дублей.
|
||||
Начальный профиль для устройства — 1000 выборок/с.
|
||||
|
||||
На UART при 921600 бод и 19 байтах × 10 бит на пакет теоретический потолок
|
||||
составляет около 4850 выборок/с, без учёта метаданных и пауз. Для непрерывной
|
||||
передачи 50 Мвыб/с этот транспорт не подходит. Высокоскоростной буферный
|
||||
захват остаётся отдельным режимом.
|
||||
|
||||
Общая C-история ограничена 8192 последними принятыми выборками. Старые данные
|
||||
вытесняются; полный длительный архив в этом режиме не ведётся. GUI обновляет
|
||||
экран не чаще 20 раз в секунду. Пауза фиксирует показанное окно, но приём
|
||||
продолжается. CSV сохраняет текущее окно с индексами, временем и признаками разрывов.
|
||||
|
||||
## API для прошивки / порта
|
||||
|
||||
### Маркеры в ONLINE GUI
|
||||
|
||||
Колесо внутри графика прокручивает его по X (влево/вправо), Ctrl + колесо —
|
||||
по Y (вверх/вниз), не меняя масштаб. Когда вся ось помещается в окно,
|
||||
прокрутка этой оси не требуется. После ручной прокрутки X автоматический
|
||||
переход к концу потока отключается до сброса масштаба.
|
||||
Qt-порт этой обработки: `python/altera_logic/plot_scroll.py`.
|
||||
|
||||
Масштабирование внутри графика: зажать ЛКМ и двигать вправо/влево для X;
|
||||
Ctrl + ЛКМ и движение вверх/вниз — для Y. Начальная точка остаётся на месте
|
||||
в видимой области. Масштаб X: 1…16, Y: 1…8. «Сброс масштаба» возвращает
|
||||
обе оси к 1. ЛКМ за линию маркера перемещает маркер; Ctrl отдаёт приоритет
|
||||
масштабу Y. Обычный клик устанавливает выбранный маркер, если он включён.
|
||||
При увеличении Y растягиваются дорожки цифровых каналов, не изменяются
|
||||
измеренные значения. Прокрутка позволяет просматривать увеличенную область.
|
||||
|
||||
Две пары X1/X2 и X3/X4 включаются независимо и показывают время каждого
|
||||
маркера, знаковый интервал Δt = X2 − X1 (либо X4 − X3) и f = 1/|Δt|.
|
||||
При совпадении маркеров частота отображается как «—». Это обратный интервал,
|
||||
а не автоматическое определение частоты сигнала.
|
||||
Две пары Y1/Y2 и Y3/Y4 включаются независимо, показывают оба положения и
|
||||
знаковую ΔY в процентах высоты графика (это не измерение напряжения).
|
||||
Панель измерений построена по примеру AndroidGUI: отдельная строка для каждой
|
||||
пары и ползунок выбранного маркера. dB для цифровых дорожек не вычисляются.
|
||||
Разности вычисляет общий C-код `set_plot.c` через `protocan.plot.PlotMath`,
|
||||
тот же код используется Android-портом.
|
||||
Линии можно перетаскивать мышью; список «Установить кликом» выбирает маркер
|
||||
для установки в любой точке. Режим «Курсор» возвращает обычный просмотр выборок.
|
||||
Снятие галочки скрывает линии, сохраняя их позиции. «Маркеры в окно» возвращает
|
||||
их в текущую область. Для измерения неподвижного фрагмента включите паузу.
|
||||
X сохраняет положение во времени при обновлении истории и может уйти за окно;
|
||||
новая сессия устанавливает начальные позиции. Визуальная модель находится
|
||||
в `python/altera_logic/markers.py`, обработка мыши и рисование — в SETGUI.
|
||||
|
||||
`c/set-protocol/include/altera_stream.h` экспортирует:
|
||||
|
||||
- `las_metadata` — общий билдер метаданных для CAN либо UART;
|
||||
- `las_data` — общий билдер выборки для CAN либо UART;
|
||||
- `las_can` / `las_uart` — два входа одного декодера и истории;
|
||||
- `las_snapshot` — хронологический снимок с индексами и разрывами;
|
||||
- `las_get` — диагностика: потери, дубли, неправильные кадры, CRC.
|
||||
|
||||
Пример формирования одной выборки для CAN, без HAL-зависимостей:
|
||||
|
||||
```c
|
||||
#include "altera_stream.h"
|
||||
uint8_t bytes[32];
|
||||
uint32_t id;
|
||||
size_t size;
|
||||
/* SessionID=1, период=1 мс. CAN-драйверу передаются id и size байт. */
|
||||
size = las_metadata(LAS_DEVICE_ID, 1, 1000000, 0, &id, bytes, sizeof(bytes));
|
||||
/* board_can_send_extended(id, bytes, size); */
|
||||
size = las_data(LAS_DEVICE_ID, 1, 0, 0x8001, 0, &id, bytes, sizeof(bytes));
|
||||
/* board_can_send_extended(id, bytes, size); */
|
||||
/* Для UART последний флаг носителя заменить с 0 на 1:
|
||||
билдер вернёт полный пакет AA55, который передаётся в UART без изменений. */
|
||||
```
|
||||
|
||||
Драйвер платы должен передавать каждый пакет полностью и сохранять порядок,
|
||||
периодически публиковать метаданные и увеличивать SampleIndex даже при потере
|
||||
пакета в своей очереди. CAN-контроллер и трансивер остаются аппаратным портом.
|
||||
|
||||
## Размещение реализации и границы
|
||||
|
||||
- `templates/c/set-protocol/src/altera_stream.c`: протокол и кольцевая история.
|
||||
- `templates/python/altera_logic/stream.py`: тонкий ctypes-порт и CSV-модель.
|
||||
- `templates/python/altera_logic/stream_port.py`: Qt UART/CAN-приём и жизненный цикл.
|
||||
- SETGUI: только визуальная вкладка и подключение к существующему CAN-источнику.
|
||||
|
||||
Текущий FPGA RTL с протоколом `A5 CMD` не становится SETCAN-источником от
|
||||
обновления GUI. Нужен передатчик этого профиля в прошивке/RTL либо внешний
|
||||
мост, который получает реальные выборки и публикует их в описанном формате.
|
||||
В этой версии реализованы GUI-приёмник, общие C-билдеры для устройства и
|
||||
демопроизводитель; FPGA RTL и физический CAN-контроллер не изменены.
|
||||
21
doc/setcan/DEVICE_REGISTRY.md
Normal file
21
doc/setcan/DEVICE_REGISTRY.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Реестр адресов SETCAN
|
||||
|
||||
Дополнения к исходной таблице `Протокол CAN и ОАП.xlsx` фиксируются здесь.
|
||||
Числовая раскладка CAN ID не меняется: DeviceType — 3 бита, DeviceID — 4 бита.
|
||||
Device Name — отображаемое имя адреса, а не дополнительное поле CAN ID.
|
||||
|
||||
| DeviceType | DeviceID | Device Name | Назначение |
|
||||
|---|---|---|---|
|
||||
| `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы |
|
||||
| `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов |
|
||||
| `0x7` | `0xD` | configurator | Существующий конфигуратор |
|
||||
| `0x7` | `0xF` | KONOR / SETTINGS | Существующий профиль SETCAN и привязка датчиков |
|
||||
|
||||
Для Altera Logic выбран предпоследний тип `0x6` и предпоследний экземпляр `0xE`:
|
||||
эта пара не была назначена в проверенных локальных таблицах и исходниках.
|
||||
Тип `0x7` уже используется. Наличие другого физического узла с тем же адресом
|
||||
на конкретной шине нужно исключить при вводе в эксплуатацию.
|
||||
|
||||
Числовые константы устройства: `c/set-protocol/include/altera_stream.h`.
|
||||
Отображаемые имена: `python/protocan/protocan.py`.
|
||||
Контракт потока: [ALTERA_LOGIC_STREAM.md](ALTERA_LOGIC_STREAM.md).
|
||||
@@ -32,3 +32,7 @@
|
||||
|
||||
Для пересборки HTML запустите `doc/setcan/build-html.bat` из корня
|
||||
репозитория `templates`.
|
||||
# Расширение Altera Logic
|
||||
|
||||
- [Реестр устройств SETCAN](DEVICE_REGISTRY.md).
|
||||
- [ONLINE-поток Altera Logic через GAS и UART](ALTERA_LOGIC_STREAM.md).
|
||||
|
||||
47
python/README.md
Normal file
47
python/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Общие Python-библиотеки SET
|
||||
|
||||
Исходники для всех приложений редактируются в репозитории `templates`.
|
||||
Потребитель подключает конкретный коммит как Git submodule либо устанавливает
|
||||
пакет из `templates/python`: `pip install -e ./python`.
|
||||
|
||||
| Пакет | Назначение |
|
||||
|---|---|
|
||||
| `logic_analyzer.files` | CSV/SAL/DSL, цифровые каналы и массивы фронтов |
|
||||
| `logic_analyzer.analysis` | UART/CAN, измерения, интервалы и события |
|
||||
| `logic_analyzer.decoders` | SET UART/CAN, PM35, 1SP0635/1SD536F2 |
|
||||
| `logic_analyzer.saleae` | Настройки, обнаружение и захват Saleae Logic 2 |
|
||||
| `altera_logic` | C FFI Altera, SETCAN stream, модели и Qt-порты |
|
||||
| `set_devices` | Кодеки, каталоги, прошивки, EEPROM/DS18B20, TMS, UMP, CAN485, спектр, демо-модели |
|
||||
| `set_devices.qt_ports` | UART/mock, SLCAN, Candle/WinUSB, STM/TMS boot, STM settings, UMP CAN |
|
||||
| `protocan`, `setprotocol` | Существующие обёртки общего C99-ядра и SET v2 |
|
||||
|
||||
Импорт `logic_analyzer` / `set_devices` не загружает Qt и не зависит от GUI.
|
||||
Qt подключается только при импорте конкретного `qt_ports` (PySide6 или PySide2).
|
||||
Saleae SDK подключается при обращении к устройству: `pip install -e './python[saleae]'`.
|
||||
Qt-виджеты, пользовательские настройки и управление сессией находятся в приложении.
|
||||
|
||||
Пути к нативным библиотекам задаёт потребитель перед импортом:
|
||||
`SETPROTOCOL_LIBRARY`, при необходимости `ALTERA_LOGIC_LIBRARY`, `CANDLE_LIBRARY`.
|
||||
Порты не ищут DLL в каталогах SETGUI. C99-ядро собирается из `c/set-protocol`,
|
||||
Candle/WinUSB — из `c/candle`. Библиотеки не выполняют поиск соседних репозиториев.
|
||||
|
||||
Пример без GUI:
|
||||
|
||||
```python
|
||||
from logic_analyzer.files import read_capture
|
||||
from logic_analyzer.analysis import analyze_capture
|
||||
|
||||
capture = read_capture('digital.csv')
|
||||
result = analyze_capture(capture, dict(mode='UART', channel=0, baudrate=115200))
|
||||
for event in result.events:
|
||||
print(event.start, event.text)
|
||||
```
|
||||
|
||||
Тесты логанализаторов без Qt и Saleae:
|
||||
`python -m unittest discover -s python/tests -p 'test_logic_analyzer_*.py'`.
|
||||
Добавьте `templates/python` в `PYTHONPATH` либо установите пакет.
|
||||
`test_shared_library_boundary.py` проверяет отсутствие зависимости от приложения.
|
||||
|
||||
Декодеры перенесены из SETGUI; исходное происхождение:
|
||||
`DSLogic_Logic_2/dslogic_script/dsview_decoders/{gate_driver_timing,set_uart,set_can,pm35_uart}`.
|
||||
После переноса единственное место сопровождения этих ядер — `templates/python/logic_analyzer`.
|
||||
1
python/altera_logic/.gitignore
vendored
Normal file
1
python/altera_logic/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
native/
|
||||
4
python/altera_logic/__init__.py
Normal file
4
python/altera_logic/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Portable Altera analyzer client. Wire protocol and sequencing live in C99."""
|
||||
from .native import Capture, NativeAnalyzer
|
||||
|
||||
__all__ = ["Capture", "NativeAnalyzer"]
|
||||
89
python/altera_logic/markers.py
Normal file
89
python/altera_logic/markers.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Presentation state for waveform rulers; no transport or wire processing."""
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
def drag_zoom_factor(delta_pixels):
|
||||
"""Visual gesture mapping: 200 pixels doubles the selected axis."""
|
||||
return 2 ** (max(-1000., min(1000., delta_pixels)) / 200.)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Marker:
|
||||
name: str
|
||||
axis: str
|
||||
value: float
|
||||
color: str
|
||||
visible: bool = False
|
||||
|
||||
|
||||
class WaveformMarkers:
|
||||
def __init__(self):
|
||||
self.items = [Marker('X1', 'x', .2, '#ffce63'),
|
||||
Marker('X2', 'x', .4, '#ffce63'),
|
||||
Marker('X3', 'x', .6, '#e794ff'),
|
||||
Marker('X4', 'x', .8, '#e794ff'),
|
||||
Marker('Y1', 'y', .2, '#72e7ff'),
|
||||
Marker('Y2', 'y', .4, '#72e7ff'),
|
||||
Marker('Y3', 'y', .6, '#a9df79'),
|
||||
Marker('Y4', 'y', .8, '#a9df79')]
|
||||
self._math = None
|
||||
self.session = None
|
||||
self.bounds = None
|
||||
|
||||
def window(self, session, start, end):
|
||||
if session != self.session or self.bounds is None:
|
||||
for marker, fraction in zip(self.items[:4], (.2, .4, .6, .8)):
|
||||
marker.value = start + (end-start)*fraction
|
||||
self.session, self.bounds = session, (start, end)
|
||||
|
||||
def reset(self):
|
||||
if self.bounds:
|
||||
start, end = self.bounds
|
||||
for marker, fraction in zip(self.items[:4], (.2, .4, .6, .8)):
|
||||
marker.value = start + (end-start)*fraction
|
||||
for marker, fraction in zip(self.items[4:], (.2, .4, .6, .8)):
|
||||
marker.value = fraction
|
||||
|
||||
def measurements(self, period_ns):
|
||||
"""Use the same native delta operation as Android's plotDelta."""
|
||||
if self._math is None:
|
||||
from protocan.plot import PlotMath
|
||||
from .native import NativeAnalyzer
|
||||
self._math = PlotMath(NativeAnalyzer().lib)
|
||||
result = []
|
||||
for index in range(0, 8, 2):
|
||||
a, b = self.items[index:index+2]
|
||||
if not (a.visible and b.visible):
|
||||
continue
|
||||
if a.axis == 'x':
|
||||
if self.bounds is None:
|
||||
continue
|
||||
factor = period_ns / 1e9
|
||||
av, bv = a.value*factor, b.value*factor
|
||||
delta = self._math.delta(a.value, b.value, factor)
|
||||
frequency = 1/abs(delta) if delta else None
|
||||
else:
|
||||
av, bv = 100*(1-a.value), 100*(1-b.value)
|
||||
delta = self._math.delta(av, bv)
|
||||
frequency = None
|
||||
result.append((a, b, av, bv, delta, frequency))
|
||||
return result
|
||||
|
||||
def place(self, index, fraction):
|
||||
marker = self.items[index]
|
||||
fraction = min(1., max(0., fraction))
|
||||
if marker.axis == 'x':
|
||||
if self.bounds is None:
|
||||
return
|
||||
start, end = self.bounds
|
||||
marker.value = start + (end-start)*fraction
|
||||
else:
|
||||
marker.value = fraction
|
||||
|
||||
def fraction(self, marker):
|
||||
if marker.axis == 'y':
|
||||
return marker.value
|
||||
if self.bounds is None:
|
||||
return 0.
|
||||
start, end = self.bounds
|
||||
return (marker.value-start)/max(1, end-start)
|
||||
131
python/altera_logic/native.py
Normal file
131
python/altera_logic/native.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""ctypes binding only: no Python packet codec or protocol fallback."""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import ctypes as C
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Capture:
|
||||
samples: tuple[int, ...]
|
||||
trigger_index: int
|
||||
sample_rate: float
|
||||
demo: bool = False
|
||||
|
||||
def save_csv(self, path):
|
||||
with open(path, "w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(["index", "time_s", "sample_hex", "demo"] +
|
||||
["D%d" % n for n in range(16)])
|
||||
for i, sample in enumerate(self.samples):
|
||||
writer.writerow([i, (i-self.trigger_index)/self.sample_rate,
|
||||
"%04X" % sample, int(self.demo)] +
|
||||
[(sample >> n) & 1 for n in range(16)])
|
||||
|
||||
|
||||
class NativeAnalyzer:
|
||||
CONNECTING, READY, CAPTURING, DONE, ERROR = range(5)
|
||||
ERROR_TEXT = {
|
||||
1: "Неверный формат ответа FPGA",
|
||||
2: "Ошибка контрольной суммы ответа",
|
||||
3: "FPGA отклонила команду",
|
||||
4: "Тайм-аут UART. При потере синхронизации нужен RESET_N и повторная настройка",
|
||||
5: "Неподдерживаемая конфигурация FPGA: ожидается 16 каналов, 4096 выборок, версия 1",
|
||||
6: "Недопустимые или противоречивые настройки триггера",
|
||||
7: "Ошибка последовательного порта",
|
||||
}
|
||||
|
||||
def __init__(self, library=None):
|
||||
if library is None:
|
||||
explicit = os.environ.get("ALTERA_LOGIC_LIBRARY") or os.environ.get("SETPROTOCOL_LIBRARY")
|
||||
name = "setprotocol.dll" if sys.platform == "win32" else "libsetprotocol.so"
|
||||
bundled = Path(getattr(sys, "_MEIPASS", "")) / "native" / name
|
||||
local = Path(__file__).resolve().parent / "native" / name
|
||||
path = Path(explicit) if explicit else (bundled if bundled.is_file() else local)
|
||||
try:
|
||||
library = C.CDLL(str(path))
|
||||
except OSError as exc:
|
||||
raise RuntimeError("Не загружено C-ядро Altera Logic. Соберите templates/"
|
||||
"c/set-protocol/tools/build_host.py; " + str(exc)) from exc
|
||||
self.lib = library
|
||||
signatures = {
|
||||
"la_context_size": ([], C.c_size_t),
|
||||
"la_init": ([C.c_void_p], None),
|
||||
"la_start": ([C.c_void_p] + [C.c_uint32]*5, C.c_int),
|
||||
"la_next": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"la_feed": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
|
||||
"la_tick": ([C.c_void_p, C.c_uint32], None),
|
||||
"la_fail": ([C.c_void_p, C.c_uint32], None),
|
||||
"la_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
|
||||
"la_samples": ([C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"la_demo_init": ([C.c_void_p], None),
|
||||
"la_demo_capture": ([C.c_void_p, C.c_uint32], C.c_int),
|
||||
}
|
||||
try:
|
||||
for name, (args, result) in signatures.items():
|
||||
fn = getattr(self.lib, name)
|
||||
fn.argtypes, fn.restype = args, result
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError("C-ядро устарело: пересоберите templates с Altera Logic") from exc
|
||||
# Explicitly aligned caller-owned storage; C never allocates memory.
|
||||
self.ctx = (C.c_uint64 * ((self.lib.la_context_size()+7)//8))()
|
||||
self.demo = False
|
||||
self.reset()
|
||||
|
||||
def reset(self, demo=False):
|
||||
self.demo = demo
|
||||
(self.lib.la_demo_init if demo else self.lib.la_init)(self.ctx)
|
||||
|
||||
def get(self, field):
|
||||
return int(self.lib.la_get(self.ctx, field))
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.get(0)
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self.ERROR_TEXT.get(self.get(1), "Ошибка обмена")
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
return self.get(7)
|
||||
|
||||
@property
|
||||
def flags(self):
|
||||
return self.get(5)
|
||||
|
||||
def start(self, divider, mask=0, value=0, edge_mask=0, edge_value=0):
|
||||
values = (divider, mask, value, edge_mask, edge_value)
|
||||
if any(not isinstance(v, int) or not 0 <= v <= 65535 for v in values):
|
||||
raise ValueError(self.ERROR_TEXT[6])
|
||||
result = (self.lib.la_demo_capture(self.ctx, divider) if self.demo else
|
||||
self.lib.la_start(self.ctx, *values))
|
||||
if result:
|
||||
raise ValueError(self.ERROR_TEXT.get(result, "Захват уже выполняется"))
|
||||
|
||||
def next_request(self):
|
||||
out = (C.c_ubyte*6)()
|
||||
size = self.lib.la_next(self.ctx, out, len(out))
|
||||
return bytes(out[:size])
|
||||
|
||||
def feed(self, data):
|
||||
self.lib.la_feed(self.ctx, data, len(data))
|
||||
|
||||
def tick(self, elapsed_ms):
|
||||
self.lib.la_tick(self.ctx, max(0, min(int(elapsed_ms), 0xffffffff)))
|
||||
|
||||
def fail_io(self):
|
||||
self.lib.la_fail(self.ctx, 7)
|
||||
|
||||
def capture(self):
|
||||
out = (C.c_uint16*4096)()
|
||||
count = self.lib.la_samples(self.ctx, out, len(out))
|
||||
if not count:
|
||||
raise RuntimeError("Запись ещё не завершена")
|
||||
return Capture(tuple(out[:count]), self.get(6),
|
||||
self.get(4)/(self.get(8)+1), self.demo)
|
||||
27
python/altera_logic/plot_scroll.py
Normal file
27
python/altera_logic/plot_scroll.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Qt presentation port: wheel pans X; Ctrl+wheel pans Y, never zooms."""
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QScrollArea
|
||||
|
||||
|
||||
class PlotScrollArea(QScrollArea):
|
||||
user_scrolled = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._remainder = {'x': 0., 'y': 0.}
|
||||
|
||||
def wheelEvent(self, event):
|
||||
axis = 'y' if event.modifiers() & Qt.KeyboardModifier.ControlModifier else 'x'
|
||||
bar = self.verticalScrollBar() if axis == 'y' else self.horizontalScrollBar()
|
||||
pixels, angle = event.pixelDelta(), event.angleDelta()
|
||||
if not pixels.isNull():
|
||||
delta = pixels.y() if pixels.y() else pixels.x()
|
||||
else:
|
||||
delta = (angle.y() if angle.y() else angle.x()) / 120. * 60.
|
||||
self._remainder[axis] -= delta
|
||||
movement = int(self._remainder[axis])
|
||||
self._remainder[axis] -= movement
|
||||
if delta:
|
||||
self.user_scrolled.emit(axis)
|
||||
bar.setValue(bar.value()+movement)
|
||||
event.accept()
|
||||
114
python/altera_logic/qt_port.py
Normal file
114
python/altera_logic/qt_port.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Qt serial/lifecycle port. Protocol decisions are exclusively in C99."""
|
||||
from __future__ import annotations
|
||||
|
||||
from set_devices.qt_ports.qt_compat import QObject, QTimer, QElapsedTimer, Signal
|
||||
from set_devices.qt_ports.qt_compat import QSerialPort, QSerialPortInfo
|
||||
from .native import NativeAnalyzer
|
||||
|
||||
|
||||
class AnalyzerPort(QObject):
|
||||
changed = Signal()
|
||||
completed = Signal(object)
|
||||
error = Signal(str)
|
||||
log = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.core = None
|
||||
self.active = False
|
||||
self._last_done = False
|
||||
self.serial = QSerialPort(self)
|
||||
self.serial.readyRead.connect(self._read)
|
||||
self.serial.errorOccurred.connect(self._serial_error)
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(10)
|
||||
self.timer.timeout.connect(self._tick)
|
||||
self.clock = QElapsedTimer()
|
||||
|
||||
@staticmethod
|
||||
def ports():
|
||||
return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
|
||||
|
||||
def open(self, name, demo=False):
|
||||
self.close()
|
||||
try:
|
||||
self.core = NativeAnalyzer()
|
||||
self.core.reset(demo)
|
||||
except RuntimeError as exc:
|
||||
self.error.emit(str(exc))
|
||||
return
|
||||
if not demo:
|
||||
self.serial.setPortName(name)
|
||||
self.serial.setBaudRate(921600)
|
||||
self.serial.setDataBits(QSerialPort.DataBits.Data8)
|
||||
self.serial.setParity(QSerialPort.Parity.NoParity)
|
||||
self.serial.setStopBits(QSerialPort.StopBits.OneStop)
|
||||
self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
|
||||
if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
|
||||
self.error.emit(self.serial.errorString())
|
||||
return
|
||||
self.active = True
|
||||
self._last_done = False
|
||||
self.clock.start()
|
||||
self.timer.start()
|
||||
self.log.emit("ДЕМО: синтетические данные, триггер не моделируется" if demo
|
||||
else "%s · 921600 8N1" % name)
|
||||
self._pump()
|
||||
|
||||
def close(self):
|
||||
self.active = False
|
||||
self.timer.stop()
|
||||
self.serial.close()
|
||||
self.changed.emit()
|
||||
|
||||
def start(self, *settings):
|
||||
if not self.active or not self.core:
|
||||
return
|
||||
try:
|
||||
self.core.start(*settings)
|
||||
except ValueError as exc:
|
||||
self.error.emit(str(exc))
|
||||
return
|
||||
self._last_done = False
|
||||
self._pump()
|
||||
|
||||
def _tick(self):
|
||||
if not self.active:
|
||||
return
|
||||
self.core.tick(self.clock.restart())
|
||||
self._pump()
|
||||
|
||||
def _read(self):
|
||||
data = bytes(self.serial.readAll())
|
||||
if self.active and data:
|
||||
self.log.emit("RX " + data.hex(" ").upper())
|
||||
self.core.feed(data)
|
||||
self._pump()
|
||||
|
||||
def _pump(self):
|
||||
if not self.active:
|
||||
return
|
||||
if self.core.state == NativeAnalyzer.ERROR:
|
||||
message = self.core.error
|
||||
self.close()
|
||||
self.error.emit(message)
|
||||
return
|
||||
packet = self.core.next_request()
|
||||
if packet:
|
||||
self.log.emit("TX " + packet.hex(" ").upper())
|
||||
if self.serial.write(packet) != len(packet):
|
||||
self.core.fail_io()
|
||||
self._pump()
|
||||
return
|
||||
self.clock.restart()
|
||||
if self.core.state == NativeAnalyzer.DONE and not self._last_done:
|
||||
self._last_done = True
|
||||
self.completed.emit(self.core.capture())
|
||||
self.changed.emit()
|
||||
|
||||
def _serial_error(self, code):
|
||||
if self.active and code != QSerialPort.SerialPortError.NoError:
|
||||
message = self.serial.errorString()
|
||||
self.core.fail_io()
|
||||
self.close()
|
||||
self.error.emit(message)
|
||||
108
python/altera_logic/stream.py
Normal file
108
python/altera_logic/stream.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""SETCAN streaming FFI and immutable display/export models. No wire codec."""
|
||||
from __future__ import annotations
|
||||
import csv
|
||||
import ctypes as C
|
||||
from dataclasses import dataclass
|
||||
from .native import NativeAnalyzer
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StreamSnapshot:
|
||||
indices: tuple[int, ...]
|
||||
samples: tuple[int, ...]
|
||||
breaks: tuple[int, ...]
|
||||
period_ns: int
|
||||
session: int
|
||||
demo: bool = False
|
||||
traces: tuple = ()
|
||||
|
||||
def save_csv(self, path):
|
||||
with open(path, "w", newline="", encoding="utf-8") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(["session", "sample_index", "time_s", "gap_before", "sample_hex", "demo"] +
|
||||
["D%d" % n for n in range(16)])
|
||||
for index, sample, gap in zip(self.indices, self.samples, self.breaks):
|
||||
writer.writerow([self.session, index, index*self.period_ns/1e9, gap,
|
||||
"%04X" % sample, int(self.demo)] +
|
||||
[(sample >> bit)&1 for bit in range(16)])
|
||||
|
||||
|
||||
class NativeStream:
|
||||
def __init__(self, device_id=None):
|
||||
self._owner = NativeAnalyzer()
|
||||
self.lib = self._owner.lib
|
||||
signatures = {
|
||||
"las_context_size": ([], C.c_size_t),
|
||||
"las_init": ([C.c_void_p, C.c_uint32], C.c_int),
|
||||
"las_can": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint32, C.c_uint32], None),
|
||||
"las_uart": ([C.c_void_p, C.c_void_p, C.c_size_t], None),
|
||||
"las_get": ([C.c_void_p, C.c_uint32], C.c_uint32),
|
||||
"las_snapshot": ([C.c_void_p, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"las_trace": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_void_p, C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"las_device_type": ([], C.c_uint32),
|
||||
"las_device_id": ([], C.c_uint32),
|
||||
"las_device_name": ([], C.c_char_p),
|
||||
"las_demo_step": ([C.c_void_p, C.c_uint32], None),
|
||||
"las_metadata": ([C.c_uint32]*4+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
"las_data": ([C.c_uint32]*5+[C.POINTER(C.c_uint32), C.c_void_p, C.c_size_t], C.c_size_t),
|
||||
}
|
||||
try:
|
||||
for name, (args, result) in signatures.items():
|
||||
fn = getattr(self.lib, name)
|
||||
fn.argtypes, fn.restype = args, result
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError("Обновите DLL templates: отсутствует SETCAN Altera stream") from exc
|
||||
self.device_type = int(self.lib.las_device_type())
|
||||
self.device_id = int(self.lib.las_device_id()) if device_id is None else device_id
|
||||
self.device_name = self.lib.las_device_name().decode("utf-8")
|
||||
self.ctx = (C.c_uint64*((self.lib.las_context_size()+7)//8))()
|
||||
if not 0 <= self.device_id <= 15 or not self.lib.las_init(self.ctx, self.device_id):
|
||||
raise ValueError("DeviceID должен быть 0…15")
|
||||
|
||||
@property
|
||||
def stats(self):
|
||||
fields = ("count", "period_ns", "session", "received", "missing", "duplicates",
|
||||
"invalid", "ignored", "crc_errors", "has_meta")
|
||||
return {name: int(self.lib.las_get(self.ctx, i)) for i, name in enumerate(fields)}
|
||||
|
||||
def feed_can(self, identifier, data, extended=True, remote=False):
|
||||
if not 0 <= identifier <= 0xffffffff:
|
||||
return
|
||||
self.lib.las_can(self.ctx, identifier, data, len(data), int(extended), int(remote))
|
||||
|
||||
def feed_uart(self, data):
|
||||
self.lib.las_uart(self.ctx, data, len(data))
|
||||
|
||||
def snapshot(self, demo=False):
|
||||
stats = self.stats
|
||||
count = stats["count"]
|
||||
indices, samples, breaks = (C.c_uint64*count)(), (C.c_uint16*count)(), (C.c_ubyte*count)()
|
||||
n = self.lib.las_snapshot(self.ctx, indices, samples, breaks, count)
|
||||
traces = []
|
||||
xs, ys, moves = (C.c_uint64*(2*count))(), (C.c_ubyte*(2*count))(), (C.c_ubyte*(2*count))()
|
||||
for channel in range(16):
|
||||
size = self.lib.las_trace(self.ctx, channel, xs, ys, moves, 2*count)
|
||||
traces.append(tuple(zip(xs[:size], ys[:size], moves[:size])))
|
||||
return StreamSnapshot(tuple(indices[:n]), tuple(samples[:n]), tuple(breaks[:n]),
|
||||
stats["period_ns"], stats["session"], demo, tuple(traces))
|
||||
|
||||
def demo_step(self, count=50):
|
||||
self.lib.las_demo_step(self.ctx, count)
|
||||
|
||||
def metadata_packet(self, session, period_ns, uart=False):
|
||||
if not 0 <= session <= 65535 or not 1 <= period_ns <= 0xffffffff:
|
||||
raise ValueError("Invalid stream metadata")
|
||||
return self._packet(self.lib.las_metadata, session, period_ns, int(uart))
|
||||
|
||||
def data_packet(self, session, index, sample, uart=False):
|
||||
if not 0 <= index <= 0xffffffff or any(not 0 <= v <= 65535 for v in (session, sample)):
|
||||
raise ValueError("Invalid stream samples")
|
||||
return self._packet(self.lib.las_data, session, index, sample, int(uart))
|
||||
|
||||
def _packet(self, fn, *args):
|
||||
identifier = C.c_uint32()
|
||||
out = (C.c_ubyte*32)()
|
||||
n = fn(self.device_id, *args, C.byref(identifier), out, len(out))
|
||||
if not n:
|
||||
raise ValueError("Invalid SETCAN packet arguments")
|
||||
return identifier.value, bytes(out[:n])
|
||||
103
python/altera_logic/stream_port.py
Normal file
103
python/altera_logic/stream_port.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Qt transport/lifecycle port for the C SETCAN stream receiver.
|
||||
|
||||
CAN ingress accepts canonical RX events from an existing bus connection.
|
||||
UART uses the same SETCAN frames inside the shared AA55/CRC16 transport.
|
||||
Reception and rendering clocks are separate; no packet-rate repainting.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from set_devices.qt_ports.qt_compat import QObject, QTimer, QElapsedTimer, Signal
|
||||
from set_devices.qt_ports.qt_compat import QSerialPort, QSerialPortInfo
|
||||
from .stream import NativeStream
|
||||
|
||||
|
||||
class StreamPort(QObject):
|
||||
updated = Signal(object, object)
|
||||
changed = Signal(bool)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.core = None
|
||||
self.active = False
|
||||
self.mode = "demo"
|
||||
self.paused = False
|
||||
self.serial = QSerialPort(self)
|
||||
self.serial.readyRead.connect(self._read)
|
||||
self.serial.errorOccurred.connect(self._error)
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(50)
|
||||
self.timer.timeout.connect(self._tick)
|
||||
self._previous = None
|
||||
self._last_data = QElapsedTimer()
|
||||
self._last_received = None
|
||||
|
||||
@staticmethod
|
||||
def ports():
|
||||
return [(p.portName(), p.description()) for p in QSerialPortInfo.availablePorts()]
|
||||
|
||||
def open(self, mode, port_name="", device_id=None):
|
||||
self.close()
|
||||
if mode not in ("demo", "can", "uart", "both"):
|
||||
self.error.emit("Неизвестный транспорт")
|
||||
return
|
||||
try:
|
||||
self.core = NativeStream(device_id)
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
self.error.emit(str(exc))
|
||||
return
|
||||
self.mode = mode
|
||||
if mode in ("uart", "both"):
|
||||
self.serial.setPortName(port_name)
|
||||
self.serial.setBaudRate(921600)
|
||||
self.serial.setDataBits(QSerialPort.DataBits.Data8)
|
||||
self.serial.setParity(QSerialPort.Parity.NoParity)
|
||||
self.serial.setStopBits(QSerialPort.StopBits.OneStop)
|
||||
self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl)
|
||||
if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite):
|
||||
self.error.emit(self.serial.errorString())
|
||||
return
|
||||
self.active = True
|
||||
self.paused = False
|
||||
self._previous = None
|
||||
self._last_received = None
|
||||
self._last_data.start()
|
||||
self.timer.start()
|
||||
self.changed.emit(True)
|
||||
|
||||
def close(self):
|
||||
self.active = False
|
||||
self.timer.stop()
|
||||
self.serial.close()
|
||||
self.changed.emit(False)
|
||||
|
||||
def receive_event(self, event):
|
||||
if (self.active and self.mode in ("can", "both") and event.get("kind") == "can"
|
||||
and event.get("direction") == "RX"):
|
||||
self.core.feed_can(event["identifier"], event["data"],
|
||||
event.get("extended", False), event.get("remote", False))
|
||||
|
||||
def _read(self):
|
||||
data = bytes(self.serial.readAll())
|
||||
if self.active and self.mode in ("uart", "both"):
|
||||
self.core.feed_uart(data)
|
||||
|
||||
def _tick(self):
|
||||
if not self.active:
|
||||
return
|
||||
if self.mode == "demo":
|
||||
self.core.demo_step()
|
||||
stats = self.core.stats
|
||||
signature = (stats["received"], stats["session"])
|
||||
if signature != self._last_received:
|
||||
self._last_received = signature
|
||||
self._last_data.restart()
|
||||
stats["stale"] = self._last_data.elapsed() > max(2000, stats["period_ns"]*3/1e6)
|
||||
if not self.paused and stats != self._previous:
|
||||
self._previous = stats
|
||||
self.updated.emit(self.core.snapshot(self.mode == "demo"), stats)
|
||||
|
||||
def _error(self, code):
|
||||
if self.active and code != QSerialPort.SerialPortError.NoError:
|
||||
message = self.serial.errorString()
|
||||
self.close()
|
||||
self.error.emit(message)
|
||||
1
python/logic_analyzer/__init__.py
Normal file
1
python/logic_analyzer/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Reusable device libraries from setcorp/templates."""
|
||||
358
python/logic_analyzer/analysis.py
Normal file
358
python/logic_analyzer/analysis.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""Offline analysis of timestamped edges; no resampling or hardware access."""
|
||||
from __future__ import annotations
|
||||
|
||||
from bisect import bisect_left, bisect_right
|
||||
from dataclasses import dataclass
|
||||
import heapq
|
||||
import math
|
||||
|
||||
from .files import ImportCancelled
|
||||
from .decoders.gate_timing import TimingChecker
|
||||
from .decoders.set_uart import StreamParser
|
||||
from .decoders.pm35_uart import PM35Parser
|
||||
from .decoders.set_can import legacy, Reassembler
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisEvent:
|
||||
start: float
|
||||
end: float
|
||||
kind: str
|
||||
text: str
|
||||
details: str = ''
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisResult:
|
||||
events: list
|
||||
truncated: bool
|
||||
start: float
|
||||
end: float
|
||||
|
||||
|
||||
def pulse_measurements(channel, time, start, end):
|
||||
"""Measure a complete adjacent high/low cycle, without scanning the record."""
|
||||
edges = channel.edges
|
||||
index = bisect_right(edges, time)
|
||||
result = {'level': channel.initial ^ (index & 1)}
|
||||
if 0 < index < len(edges):
|
||||
left, right = edges[index - 1], edges[index]
|
||||
if start <= left < right <= end:
|
||||
result['width'] = right - left
|
||||
result['high' if result['level'] else 'low'] = right - left
|
||||
# Prefer the next complete interval, otherwise the preceding one.
|
||||
other = None
|
||||
if index + 1 < len(edges) and edges[index + 1] <= end:
|
||||
other = edges[index + 1] - right
|
||||
elif index >= 2 and edges[index - 2] >= start:
|
||||
other = left - edges[index - 2]
|
||||
if other is not None:
|
||||
result['low' if result['level'] else 'high'] = other
|
||||
result['period'] = result['high'] + result['low']
|
||||
result['frequency'] = 1 / result['period']
|
||||
result['duty'] = 100 * result['high'] / result['period']
|
||||
return result
|
||||
|
||||
|
||||
def interval_edges(channel, a, b):
|
||||
"""Count rising/falling edges in (min(a,b), max(a,b)]."""
|
||||
left, right = sorted((a, b))
|
||||
lo, hi = bisect_right(channel.edges, left), bisect_right(channel.edges, right)
|
||||
count = hi - lo
|
||||
first_rising = channel.initial ^ (lo & 1) == 0
|
||||
rising = count // 2 + int(bool(count & 1) and first_rising)
|
||||
return rising, count - rising
|
||||
|
||||
|
||||
def _cancel(cancel):
|
||||
if cancel():
|
||||
raise ImportCancelled()
|
||||
|
||||
|
||||
def uart_frames(channel, start, end, baud, parity='none', stops=1, inverted=False, cancel=lambda: False):
|
||||
"""Yield (start, end, byte, error), sampling 8-bit UART at bit centres."""
|
||||
bit = 1.0 / baud
|
||||
bits = 9 + (parity != 'none') + stops
|
||||
edges = channel.edges
|
||||
index = bisect_left(edges, start)
|
||||
while index < len(edges) and edges[index] < end:
|
||||
_cancel(cancel)
|
||||
time = edges[index]
|
||||
level = channel.initial ^ ((index + 1) & 1) ^ inverted
|
||||
if level:
|
||||
index += 1
|
||||
continue
|
||||
finish = time + bits * bit
|
||||
if finish > end + bit * 1e-6:
|
||||
yield time, end, None, 'Неполный UART-байт в конце диапазона'
|
||||
break
|
||||
read = lambda pos: channel.level_at(time + pos * bit) ^ inverted
|
||||
if read(0.5):
|
||||
index += 1 # A glitch shorter than half a start bit.
|
||||
continue
|
||||
values = [read(1.5 + i) for i in range(8)]
|
||||
value = sum(v << i for i, v in enumerate(values))
|
||||
error = None
|
||||
if parity != 'none' and read(9.5) != ((sum(values) + (parity == 'odd')) & 1):
|
||||
error = 'Ошибка чётности UART'
|
||||
stop_start = 9 + (parity != 'none')
|
||||
if any(read(stop_start + i + 0.5) != 1 for i in range(stops)):
|
||||
error = 'Ошибка стопового бита UART / BREAK'
|
||||
yield time, finish, value, error
|
||||
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
|
||||
|
||||
|
||||
def _number(bits):
|
||||
value = 0
|
||||
for bit in bits:
|
||||
value = (value << 1) | bit
|
||||
return value
|
||||
|
||||
|
||||
def can_crc(bits):
|
||||
crc = 0
|
||||
for bit in bits:
|
||||
feedback = ((crc >> 14) & 1) ^ bit
|
||||
crc = (crc << 1) & 0x7fff
|
||||
if feedback:
|
||||
crc ^= 0x4599
|
||||
return crc
|
||||
|
||||
|
||||
class _CanBits:
|
||||
def __init__(self, channel, time, end, bitrate, inverted, sample_point):
|
||||
self.channel, self.time, self.end = channel, time, end
|
||||
self.bit, self.inverted = 1 / bitrate, inverted
|
||||
self.sample_point = sample_point
|
||||
self.position, self.previous, self.run = 0, None, 0
|
||||
self.bits = []
|
||||
self.last_sample = time
|
||||
|
||||
def raw(self):
|
||||
# Resynchronise on recessive->dominant edges near the bit boundary.
|
||||
boundary = self.time + self.position * self.bit
|
||||
if self.position:
|
||||
lo = bisect_right(self.channel.edges, max(self.last_sample, boundary - self.bit * .2))
|
||||
hi = bisect_right(self.channel.edges, boundary + self.bit * .2, lo)
|
||||
for index in range(lo, hi):
|
||||
level = self.channel.initial ^ ((index + 1) & 1) ^ self.inverted
|
||||
if level == 0:
|
||||
self.time += self.channel.edges[index] - boundary
|
||||
boundary = self.channel.edges[index]
|
||||
break
|
||||
sample = boundary + self.sample_point * self.bit
|
||||
if sample >= self.end:
|
||||
raise EOFError('Неполный CAN-кадр в конце диапазона')
|
||||
self.last_sample = sample
|
||||
self.position += 1
|
||||
return self.channel.level_at(sample) ^ self.inverted
|
||||
|
||||
def stuffed(self):
|
||||
if self.run == 5:
|
||||
value = self.raw()
|
||||
if value == self.previous:
|
||||
raise ValueError('CAN: ошибка bit stuffing / error frame')
|
||||
self.previous, self.run = value, 1
|
||||
value = self.raw()
|
||||
self.run = self.run + 1 if value == self.previous else 1
|
||||
self.previous = value
|
||||
self.bits.append(value)
|
||||
return value
|
||||
|
||||
def take(self, count):
|
||||
return _number([self.stuffed() for _ in range(count)])
|
||||
|
||||
|
||||
def can_frames(channel, start, end, bitrate, inverted=False, sample_point=.7, cancel=lambda: False):
|
||||
"""Classic CAN, strict stuffing/CRC/form validation before application decode."""
|
||||
edges = channel.edges
|
||||
index = bisect_left(edges, start)
|
||||
bit = 1 / bitrate
|
||||
while index < len(edges) and edges[index] < end:
|
||||
_cancel(cancel)
|
||||
time = edges[index]
|
||||
level = channel.initial ^ ((index + 1) & 1) ^ inverted
|
||||
previous = edges[index - 1] if index else start
|
||||
# SOF follows at least three recessive intermission bits.
|
||||
if level or time - previous < bit * 2.9:
|
||||
index += 1
|
||||
continue
|
||||
reader = _CanBits(channel, time, end, bitrate, inverted, sample_point)
|
||||
frame, error = None, None
|
||||
try:
|
||||
if reader.take(1):
|
||||
raise ValueError('CAN: неверный SOF')
|
||||
ident = reader.take(11)
|
||||
rtr, extended = reader.take(1), reader.take(1)
|
||||
if extended:
|
||||
if rtr != 1:
|
||||
raise ValueError('CAN: неверный SRR')
|
||||
ident = (ident << 18) | reader.take(18)
|
||||
rtr = reader.take(1)
|
||||
if reader.take(2):
|
||||
raise ValueError('CAN FD / reserved bits не поддерживаются')
|
||||
elif reader.take(1):
|
||||
raise ValueError('CAN FD / reserved bit не поддерживается')
|
||||
dlc = reader.take(4)
|
||||
if dlc > 8:
|
||||
raise ValueError('CAN: поддерживается classic DLC 0…8')
|
||||
data = bytes(reader.take(8) for _ in range(0 if rtr else dlc))
|
||||
expected = can_crc(reader.bits)
|
||||
received = reader.take(15)
|
||||
if reader.run == 5:
|
||||
if reader.raw() == reader.previous:
|
||||
raise ValueError('CAN: неверный последний stuff bit')
|
||||
if reader.raw() != 1:
|
||||
raise ValueError('CAN: неверный CRC delimiter')
|
||||
ack = reader.raw() == 0
|
||||
if reader.raw() != 1 or any(reader.raw() != 1 for _ in range(7)):
|
||||
raise ValueError('CAN: неверный ACK delimiter / EOF')
|
||||
if received != expected:
|
||||
raise ValueError('CAN CRC15: получено %04X, ожидается %04X' % (received, expected))
|
||||
frame = dict(ident=ident, extended=bool(extended), remote=bool(rtr),
|
||||
data=data, dlc=dlc, ack=ack)
|
||||
except (ValueError, EOFError) as exc:
|
||||
error = str(exc)
|
||||
finish = min(end, reader.time + reader.position * bit)
|
||||
yield time, finish, frame, error
|
||||
index = bisect_left(edges, finish - bit * 1e-6, index + 1)
|
||||
|
||||
|
||||
def analyze_capture(capture, options, start=None, end=None, progress=lambda n: None, cancel=lambda: False):
|
||||
start = capture.start if start is None else max(capture.start, start)
|
||||
end = capture.end if end is None else min(capture.end, end)
|
||||
if end <= start:
|
||||
raise ValueError('Выберите непустой интервал анализа.')
|
||||
mode = options['mode']
|
||||
channel = capture.channels[options.get('channel', 0)]
|
||||
events = []
|
||||
last_progress = -1
|
||||
limit = options.get('max_events', 50000)
|
||||
if limit <= 0:
|
||||
raise ValueError('Лимит результатов должен быть положительным.')
|
||||
|
||||
def emit(a, b, kind, text, details=''):
|
||||
if len(events) >= limit:
|
||||
raise OverflowError()
|
||||
events.append(AnalysisEvent(a, b, kind, text, details))
|
||||
|
||||
def report(time):
|
||||
nonlocal last_progress
|
||||
_cancel(cancel)
|
||||
value = min(99, int((time - start) / (end - start) * 100))
|
||||
if value != last_progress:
|
||||
progress(value)
|
||||
last_progress = value
|
||||
|
||||
def parsed(items):
|
||||
for a, b, frame, error in items:
|
||||
emit(a, b, 'error' if error else 'frame', error or frame['summary'],
|
||||
'' if not frame else bytes(frame.get('raw', frame.get('payload', b''))).hex(' '))
|
||||
if frame and frame.get('protocol') == 'ProtoCAN bridge' and frame['flags'] & 1 and not frame['flags'] & 10:
|
||||
try:
|
||||
application = legacy(frame['can_id'], frame['payload'])
|
||||
emit(a, b, 'frame', application['summary'])
|
||||
except ValueError as exc:
|
||||
emit(a, b, 'error', str(exc))
|
||||
|
||||
truncated = False
|
||||
try:
|
||||
if mode in ('1SP0635', '1SD536F2'):
|
||||
other = options.get('status_channel', 1)
|
||||
if other == options.get('channel', 0):
|
||||
raise ValueError('Vin и Vstat должны быть разными каналами.')
|
||||
status = capture.channels[other]
|
||||
checker = TimingChecker(1e9, mode, options.get('tolerance_ns', 100),
|
||||
not options.get('vin_low', False), not options.get('status_low', False))
|
||||
# The pure checker works in integer ticks. Offset before rounding
|
||||
# to preserve ns precision for CSV timestamps far from zero.
|
||||
tick = lambda t: round((t - start) * 1e9)
|
||||
|
||||
def timing(items):
|
||||
for item in items:
|
||||
text = item['text']
|
||||
if item['kind'] == 'fault':
|
||||
text += ' · аварийная обратная связь; превышение тока не подтверждено'
|
||||
emit(start + item['start'] / 1e9, start + item['end'] / 1e9, item['kind'], text)
|
||||
|
||||
def transitions(ch, which):
|
||||
first = bisect_right(ch.edges, start)
|
||||
for i in range(first, bisect_right(ch.edges, end)):
|
||||
yield ch.edges[i], which, ch.initial ^ ((i + 1) & 1)
|
||||
|
||||
if bool(status.level_at(start)) == checker.vstat_active_high:
|
||||
emit(start, start, 'orphan', 'Vstat активен в начале диапазона: начало импульса не записано')
|
||||
for time, which, level in heapq.merge(transitions(channel, 0), transitions(status, 1)):
|
||||
report(time)
|
||||
timing(checker.expire(tick(time)))
|
||||
timing(checker.on_control_edge(tick(time), level) if which == 0 else checker.on_status_edge(tick(time), level))
|
||||
timing(checker.expire(tick(end)))
|
||||
if checker.status_start is not None:
|
||||
emit(start + checker.status_start / 1e9, end, 'incomplete', 'Vstat: импульс не завершён в диапазоне')
|
||||
for pending in checker.pending:
|
||||
emit(start + pending['sample'] / 1e9, end, 'incomplete', 'Vin: диапазон закончился до тайм-аута ACK')
|
||||
elif mode in ('UART', 'SET UART', 'PM35 UART'):
|
||||
baud = options.get('baudrate', 115200)
|
||||
parity, stops = options.get('parity', 'none'), options.get('stops', 1)
|
||||
if not math.isfinite(baud) or baud <= 0 or parity not in ('none', 'even', 'odd') or stops not in (1, 2):
|
||||
raise ValueError('Некорректные настройки UART.')
|
||||
parser = (StreamParser(options.get('protocol', 'auto')) if mode == 'SET UART' else
|
||||
PM35Parser(options.get('role', 'response')) if mode == 'PM35 UART' else None)
|
||||
gap = (3.5 * (9 + (parity != 'none') + stops) / baud if mode == 'PM35 UART'
|
||||
else options.get('gap_ms', 100) / 1000)
|
||||
last = None
|
||||
for a, b, value, error in uart_frames(channel, start, end, baud, parity, stops, options.get('inverted', False), cancel):
|
||||
report(a)
|
||||
if parser and last is not None and gap > 0 and a - last >= gap:
|
||||
parsed(parser.flush())
|
||||
last = b
|
||||
if error:
|
||||
if parser:
|
||||
parsed(parser.flush())
|
||||
emit(a, b, 'error', error)
|
||||
elif parser:
|
||||
parsed(parser.feed(value, a, b))
|
||||
else:
|
||||
emit(a, b, 'byte', 'UART 0x%02X' % value, chr(value) if 32 <= value < 127 else '')
|
||||
if parser:
|
||||
parsed(parser.flush())
|
||||
elif mode in ('CAN', 'SET CAN'):
|
||||
bitrate = options.get('baudrate', 1000000)
|
||||
sample_point = options.get('sample_point', 70) / 100
|
||||
if not math.isfinite(bitrate) or bitrate <= 0 or not .1 <= sample_point <= .95:
|
||||
raise ValueError('Некорректный битрейт / точка выборки CAN.')
|
||||
reassembler = Reassembler()
|
||||
for a, b, frame, error in can_frames(channel, start, end, bitrate, options.get('inverted', False), sample_point, cancel):
|
||||
report(a)
|
||||
if error:
|
||||
emit(a, b, 'error', error)
|
||||
# Never bridge an invalid/missing physical frame.
|
||||
for pending in reassembler.pending.values():
|
||||
emit(pending['start'], b, 'incomplete', 'SET CAN: сборка прервана ошибкой шины')
|
||||
reassembler.pending.clear()
|
||||
continue
|
||||
ident, data = frame['ident'], frame['data']
|
||||
emit(a, b, 'can', 'CAN %s ID=%08X DLC=%d %s %s' % (
|
||||
'EXT' if frame['extended'] else 'STD', ident, frame['dlc'],
|
||||
'RTR' if frame['remote'] else 'DATA', 'ACK' if frame['ack'] else 'NACK'), data.hex(' '))
|
||||
if mode == 'SET CAN' and frame['extended'] and not frame['remote']:
|
||||
protocol = options.get('protocol', 'protocan')
|
||||
if protocol == 'set-v2':
|
||||
parsed(reassembler.feed(ident, data, a, b, b * 1000))
|
||||
else:
|
||||
try:
|
||||
result = legacy(ident, data, protocol)
|
||||
if result:
|
||||
emit(a, b, 'frame', result['summary'], data.hex(' '))
|
||||
except ValueError as exc:
|
||||
emit(a, b, 'error', str(exc))
|
||||
for pending in reassembler.pending.values():
|
||||
emit(pending['start'], end, 'incomplete', 'Неполная сборка SET CAN в конце диапазона')
|
||||
else:
|
||||
raise ValueError('Неизвестный анализатор: ' + mode)
|
||||
except OverflowError:
|
||||
truncated = True
|
||||
_cancel(cancel)
|
||||
progress(100)
|
||||
events.sort(key=lambda event: (event.start, event.end))
|
||||
return AnalysisResult(events, truncated, start, end)
|
||||
1
python/logic_analyzer/decoders/__init__.py
Normal file
1
python/logic_analyzer/decoders/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Reusable device libraries from setcorp/templates."""
|
||||
181
python/logic_analyzer/decoders/gate_timing.py
Normal file
181
python/logic_analyzer/decoders/gate_timing.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Pure timing checker used by the DSView decoder and unit tests.
|
||||
|
||||
All time values in profiles are nanoseconds. The ACK delay in the vendor
|
||||
data sheets is a typical value only, so the delay window is intentionally an
|
||||
engineering warning threshold. ACK pulse width has specified limits and is
|
||||
therefore checked as PASS/FAIL.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
PROFILES = {
|
||||
'1SP0635': {
|
||||
'ack_delay_typ_ns': 250.0,
|
||||
'ack_width_min_ns': 400.0,
|
||||
'ack_width_typ_ns': 700.0,
|
||||
'ack_width_max_ns': 1050.0,
|
||||
'fault_threshold_ns': 1500.0,
|
||||
},
|
||||
'1SD536F2': {
|
||||
'ack_delay_typ_ns': 380.0,
|
||||
'ack_width_min_ns': 600.0,
|
||||
'ack_width_typ_ns': 900.0,
|
||||
'ack_width_max_ns': 1800.0,
|
||||
# The application manual calls >1.5 us a fault, while individual
|
||||
# data sheets allow ACK pulses up to 1.8 us. A correlated pulse in
|
||||
# the specified ACK range wins; otherwise the longer limit is used.
|
||||
'fault_threshold_ns': 1800.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def format_ns(value_ns):
|
||||
if value_ns >= 1000000.0:
|
||||
return '%.3f ms' % (value_ns / 1000000.0)
|
||||
if value_ns >= 1000.0:
|
||||
return '%.3f us' % (value_ns / 1000.0)
|
||||
return '%.1f ns' % value_ns
|
||||
|
||||
|
||||
class TimingChecker(object):
|
||||
"""Match Vin edges to Vstat pulses and return annotation dictionaries."""
|
||||
|
||||
def __init__(self, samplerate, profile='1SP0635', delay_tolerance_ns=100.0,
|
||||
vin_active_high=True, vstat_active_high=True,
|
||||
custom=None):
|
||||
if not samplerate:
|
||||
raise ValueError('samplerate is required')
|
||||
if profile == 'custom':
|
||||
if not custom:
|
||||
raise ValueError('custom profile values are required')
|
||||
self.spec = dict(custom)
|
||||
else:
|
||||
self.spec = dict(PROFILES[profile])
|
||||
self.profile = profile
|
||||
self.samplerate = float(samplerate)
|
||||
self.delay_tolerance_ns = float(delay_tolerance_ns)
|
||||
self.vin_active_high = bool(vin_active_high)
|
||||
self.vstat_active_high = bool(vstat_active_high)
|
||||
self.pending = deque()
|
||||
self.status_start = None
|
||||
self.status_control = None
|
||||
|
||||
# Long enough to avoid calling a merely late ACK "missing", while
|
||||
# still producing a useful annotation during a capture.
|
||||
self.ack_timeout_ns = max(
|
||||
self.spec['ack_delay_typ_ns'] + 3.0 * self.delay_tolerance_ns,
|
||||
self.spec['ack_delay_typ_ns'] + self.spec['ack_width_max_ns'])
|
||||
|
||||
def samples_to_ns(self, samples):
|
||||
return float(samples) * 1000000000.0 / self.samplerate
|
||||
|
||||
def ns_to_samples(self, value_ns):
|
||||
return max(1, int(round(float(value_ns) * self.samplerate / 1000000000.0)))
|
||||
|
||||
def next_deadline(self):
|
||||
if not self.pending:
|
||||
return None
|
||||
return self.pending[0]['sample'] + self.ns_to_samples(self.ack_timeout_ns)
|
||||
|
||||
def on_control_edge(self, sample, level):
|
||||
state_on = bool(level) == self.vin_active_high
|
||||
item = {
|
||||
'sample': int(sample),
|
||||
'level': int(level),
|
||||
'edge': 'ON' if state_on else 'OFF',
|
||||
}
|
||||
self.pending.append(item)
|
||||
return [{
|
||||
'kind': 'control', 'start': int(sample), 'end': int(sample),
|
||||
'text': 'Vin %s' % item['edge'], 'short': item['edge'],
|
||||
}]
|
||||
|
||||
def expire(self, sample):
|
||||
events = []
|
||||
timeout_samples = self.ns_to_samples(self.ack_timeout_ns)
|
||||
while self.pending and int(sample) >= self.pending[0]['sample'] + timeout_samples:
|
||||
item = self.pending.popleft()
|
||||
end = item['sample'] + timeout_samples
|
||||
events.append({
|
||||
'kind': 'missing', 'start': item['sample'], 'end': end,
|
||||
'text': 'FAIL: no Vstat ACK after Vin %s (timeout %s)' %
|
||||
(item['edge'], format_ns(self.ack_timeout_ns)),
|
||||
'short': 'NO ACK',
|
||||
})
|
||||
return events
|
||||
|
||||
def on_status_edge(self, sample, level):
|
||||
sample = int(sample)
|
||||
is_active = bool(level) == self.vstat_active_high
|
||||
events = self.expire(sample)
|
||||
|
||||
if is_active:
|
||||
# Ignore a second active edge caused by an inconsistent trace.
|
||||
if self.status_start is not None:
|
||||
return events
|
||||
self.status_start = sample
|
||||
self.status_control = self.pending.popleft() if self.pending else None
|
||||
if self.status_control is not None:
|
||||
delay_ns = self.samples_to_ns(sample - self.status_control['sample'])
|
||||
typ_ns = self.spec['ack_delay_typ_ns']
|
||||
delta_ns = delay_ns - typ_ns
|
||||
in_window = abs(delta_ns) <= self.delay_tolerance_ns
|
||||
events.append({
|
||||
'kind': 'delay_ok' if in_window else 'delay_warn',
|
||||
'start': self.status_control['sample'], 'end': sample,
|
||||
'delay_ns': delay_ns,
|
||||
'text': '%s: ACK delay %s (typ %s, delta %+0.1f ns)' %
|
||||
('PASS' if in_window else 'WARN', format_ns(delay_ns),
|
||||
format_ns(typ_ns), delta_ns),
|
||||
'short': '%s %s' % ('OK' if in_window else 'WARN',
|
||||
format_ns(delay_ns)),
|
||||
})
|
||||
return events
|
||||
|
||||
if self.status_start is None:
|
||||
events.append({
|
||||
'kind': 'orphan', 'start': sample, 'end': sample,
|
||||
'text': 'Unexpected inactive Vstat edge', 'short': 'Vstat?',
|
||||
})
|
||||
return events
|
||||
|
||||
start = self.status_start
|
||||
control = self.status_control
|
||||
width_ns = self.samples_to_ns(sample - start)
|
||||
self.status_start = None
|
||||
self.status_control = None
|
||||
lo = self.spec['ack_width_min_ns']
|
||||
hi = self.spec['ack_width_max_ns']
|
||||
|
||||
if control is not None and lo <= width_ns <= hi:
|
||||
events.append({
|
||||
'kind': 'width_ok', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'PASS: ACK width %s (limit %s...%s)' %
|
||||
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
|
||||
'short': 'ACK %s' % format_ns(width_ns),
|
||||
})
|
||||
elif width_ns > self.spec['fault_threshold_ns']:
|
||||
events.append({
|
||||
'kind': 'fault', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'FAULT: Vstat active for %s' % format_ns(width_ns),
|
||||
'short': 'FAULT %s' % format_ns(width_ns),
|
||||
})
|
||||
elif control is None:
|
||||
events.append({
|
||||
'kind': 'orphan', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'Unexpected Vstat pulse %s (no Vin edge)' % format_ns(width_ns),
|
||||
'short': 'ORPHAN %s' % format_ns(width_ns),
|
||||
})
|
||||
else:
|
||||
events.append({
|
||||
'kind': 'width_fail', 'start': start, 'end': sample,
|
||||
'width_ns': width_ns,
|
||||
'text': 'FAIL: ACK width %s outside %s...%s' %
|
||||
(format_ns(width_ns), format_ns(lo), format_ns(hi)),
|
||||
'short': 'BAD ACK %s' % format_ns(width_ns),
|
||||
})
|
||||
return events
|
||||
95
python/logic_analyzer/decoders/pm35_uart.py
Normal file
95
python/logic_analyzer/decoders/pm35_uart.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""PM35 MODBUS 03/06, including the legacy 128-word/zero-byte-count reply."""
|
||||
from .set_uart import crc16, number
|
||||
|
||||
|
||||
def parse_pm35(raw, role):
|
||||
raw = bytes(raw)
|
||||
if len(raw) < 5:
|
||||
raise ValueError('Incomplete PM35 frame')
|
||||
if crc16(raw[:-2], True) != number(raw[-2:]):
|
||||
raise ValueError('PM35 CRC mismatch')
|
||||
address, function = raw[:2]
|
||||
result = dict(protocol='PM35', controller=address, function=function, raw=raw)
|
||||
if function in (0x83, 0x86):
|
||||
if len(raw) != 5 or role == 'request':
|
||||
raise ValueError('Invalid exception response')
|
||||
result['summary'] = 'PM35 node=%d exception fn=%02X code=%d' % (address, function, raw[2])
|
||||
elif function == 6 or function == 3 and role == 'request':
|
||||
if len(raw) != 8:
|
||||
raise ValueError('PM35 request/06 must contain 8 bytes')
|
||||
register, value = number(raw[2:4], 'big'), number(raw[4:6], 'big')
|
||||
if register >= 128 or function == 3 and not 1 <= value <= 128 - register:
|
||||
raise ValueError('PM35 register range outside 0..127')
|
||||
result.update(register=register, value=value)
|
||||
result['summary'] = 'PM35 node=%d %s R%d %s=%d' % (address,
|
||||
'READ' if function == 3 else 'WRITE/echo', register,
|
||||
'count' if function == 3 else 'value', value)
|
||||
if function == 6 and register == 127:
|
||||
result['summary'] += ' command_bits=%04X' % value
|
||||
elif function == 3:
|
||||
count = raw[2] or 256
|
||||
if count % 2 or len(raw) != count + 5:
|
||||
raise ValueError('Invalid PM35 response byte count')
|
||||
result['values'] = [number(raw[i:i+2], 'big') for i in range(3, len(raw)-2, 2)]
|
||||
result['summary'] = 'PM35 node=%d READ response %d words: %s' % (address,
|
||||
len(result['values']), ' '.join('%04X' % v for v in result['values']))
|
||||
else:
|
||||
raise ValueError('Unsupported PM35 function')
|
||||
return result
|
||||
|
||||
|
||||
class PM35Parser:
|
||||
def __init__(self, role):
|
||||
self.role = role
|
||||
self.buffer = []
|
||||
|
||||
def flush(self):
|
||||
if not self.buffer:
|
||||
return []
|
||||
ss, es = self.buffer[0][1], self.buffer[-1][2]
|
||||
self.buffer = []
|
||||
return [(ss, es, None, 'Incomplete PM35 frame')]
|
||||
|
||||
def feed(self, value, ss, es):
|
||||
self.buffer.append((value, ss, es))
|
||||
events = []
|
||||
while len(self.buffer) >= 3:
|
||||
raw = bytes(item[0] for item in self.buffer)
|
||||
fn = raw[1]
|
||||
if fn not in (3, 6, 0x83, 0x86):
|
||||
del self.buffer[0]
|
||||
continue
|
||||
role = self.role
|
||||
total = 5 if fn & 128 else 8
|
||||
if role == 'auto' and fn == 3:
|
||||
candidates = [(8, 'request'), ((raw[2] or 256) + 5, 'response')]
|
||||
found = None
|
||||
for length, candidate_role in candidates:
|
||||
if len(raw) >= length:
|
||||
try:
|
||||
found = (length, candidate_role, parse_pm35(raw[:length], candidate_role))
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
if found:
|
||||
length, candidate_role, result = found
|
||||
events.append((self.buffer[0][1], self.buffer[length-1][2], result, None))
|
||||
del self.buffer[:length]
|
||||
continue
|
||||
if len(raw) < max(length for length, _ in candidates):
|
||||
break
|
||||
events.append((self.buffer[0][1], self.buffer[-1][2], None, 'Invalid PM35 frame/CRC'))
|
||||
del self.buffer[0]
|
||||
continue
|
||||
if fn == 3 and role == 'response':
|
||||
total = (raw[2] or 256) + 5
|
||||
if len(raw) < total:
|
||||
break
|
||||
result, error = None, None
|
||||
try:
|
||||
result = parse_pm35(raw[:total], role)
|
||||
except ValueError as exc:
|
||||
error = str(exc)
|
||||
events.append((self.buffer[0][1], self.buffer[total-1][2], result, error))
|
||||
del self.buffer[:1 if error else total]
|
||||
return events
|
||||
124
python/logic_analyzer/decoders/set_can.py
Normal file
124
python/logic_analyzer/decoders/set_can.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""ProtoCAN, Balsam and SET v2 classic CAN binding."""
|
||||
from .set_uart import number, parse_frame
|
||||
|
||||
TYPES = {0: 'BROADCAST', 1: 'DISCRETE', 2: 'ANALOG', 3: 'GAS',
|
||||
4: 'MODBUS_COIL', 5: 'MODBUS_DISCRETE', 6: 'MODBUS_HOLDING',
|
||||
7: 'MODBUS_INPUT', 8: 'ERROR', 9: 'BOOT_CONTROL', 10: 'BOOT_DATA_A',
|
||||
11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'}
|
||||
BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIFY '
|
||||
'COMMIT CONFIRM REBOOT ABORT QUERY_PROGRESS').split(), 1))
|
||||
|
||||
|
||||
def legacy(ident, data, profile='protocan'):
|
||||
if not 0 <= ident <= 0x1fffffff or len(data) > 8:
|
||||
raise ValueError('Invalid classic CAN frame')
|
||||
relative = ident - 0xba0000
|
||||
if profile == 'balsam':
|
||||
if not (0 <= relative < 13 or 16 <= relative < 29):
|
||||
return None
|
||||
if len(data) != 8:
|
||||
raise ValueError('Balsam requires DLC=8')
|
||||
header = number(data[:2], 'big')
|
||||
address, mask = header & 0x1fff, header >> 13
|
||||
registers = [(address + i, number(data[2+i*2:4+i*2], 'big'))
|
||||
for i in range(3) if mask & (4 >> i)]
|
||||
return dict(protocol='Balsam', registers=registers, summary=
|
||||
'Balsam node=%d %s mask=%X %s' % ((relative & 15) + 1,
|
||||
'data' if relative >= 16 else 'command', mask,
|
||||
' '.join('R%04X=%04X' % pair for pair in registers)))
|
||||
msg, body = (ident >> 16) & 15, ident & 0xffff
|
||||
result = dict(protocol='ProtoCAN', priority=(ident >> 28) & 1,
|
||||
route=(ident >> 27) & 1, device_type=(ident >> 24) & 7,
|
||||
device_id=(ident >> 20) & 15, message_type=msg, body=body)
|
||||
text = 'ProtoCAN %s dev=%d:%d route=%d priority=%d body=%04X' % (
|
||||
TYPES.get(msg, 'TYPE_%X' % msg), result['device_type'], result['device_id'],
|
||||
result['route'], result['priority'], body)
|
||||
if msg == 3:
|
||||
if len(data) % 2 or body + len(data)//2 > 65536:
|
||||
raise ValueError('Invalid GAS register payload/range')
|
||||
result['registers'] = [(body + i//2, number(data[i:i+2])) for i in range(0, len(data), 2)]
|
||||
text += ' ' + ('READ' if not data else ' '.join('R%04X=%04X' % pair for pair in result['registers']))
|
||||
elif 4 <= msg <= 7:
|
||||
text += ' address=%03X count=%d' % (body >> 4, body & 15)
|
||||
elif msg == 8:
|
||||
text += ' info=%02X code=%02X' % (body >> 8, body & 255)
|
||||
elif msg in (9, 12):
|
||||
command = body & 255
|
||||
text += ' session=%d command=%s' % (body >> 8, BOOT.get(command, '%02X' % command))
|
||||
if msg == 12:
|
||||
if len(data) != 8:
|
||||
raise ValueError('BOOT_STATUS requires DLC=8')
|
||||
text += ' status=%d slot=%d next_block=%d CRC32=%08X' % (data[0], data[1], number(data[2:4]), number(data[4:8]))
|
||||
elif command in (3, 4):
|
||||
if len(data) != 8:
|
||||
raise ValueError('BOOT metadata requires DLC=8')
|
||||
if command == 3:
|
||||
text += ' image_size=%d CRC32=%08X' % (number(data[:4]), number(data[4:]))
|
||||
else:
|
||||
text += ' product=%d hw=%d..%d version=%08X' % (number(data[:2]), data[2], data[3], number(data[4:]))
|
||||
elif command in BOOT and data:
|
||||
raise ValueError('BOOT control requires DLC=0')
|
||||
elif msg in (10, 11):
|
||||
if len(data) != 8:
|
||||
raise ValueError('BOOT_DATA requires DLC=8')
|
||||
text += ' block=%d offset=0x%X' % (body, body * 8)
|
||||
elif msg in (0, 1, 2):
|
||||
shift = 4 if msg == 0 else 12
|
||||
text += ' type=%X value=%X' % (body >> shift, body & ((1 << shift) - 1))
|
||||
result['summary'] = text
|
||||
return result
|
||||
|
||||
|
||||
class Reassembler:
|
||||
def __init__(self):
|
||||
self.pending = {}
|
||||
|
||||
def feed(self, ident, data, ss, es, now_ms):
|
||||
events = []
|
||||
for key, state in list(self.pending.items()):
|
||||
if now_ms - state['time'] >= 500:
|
||||
events.append((state['start'], es, None, 'SET CAN timeout ID=%08X' % key))
|
||||
del self.pending[key]
|
||||
if ident >> 24 != 0x12:
|
||||
return events
|
||||
try:
|
||||
if not 1 <= len(data) <= 8:
|
||||
raise ValueError('Invalid SET CAN DLC')
|
||||
pci = data[0] & 0xf0
|
||||
if pci == 0x30:
|
||||
if len(data) != 3 or data[0] & 15 > 2 or data[2] > 127:
|
||||
raise ValueError('Invalid FLOW_CONTROL')
|
||||
events.append((ss, es, dict(protocol='SET CAN', summary='FLOW_CONTROL status=%d block=%d st_min=%d ms' % (data[0] & 15, data[1], data[2])), None))
|
||||
elif pci == 0x10:
|
||||
if data[0] != 0x10 or len(data) != 8 or not 18 <= number(data[1:3]) <= 530:
|
||||
raise ValueError('Invalid FIRST segment')
|
||||
old = self.pending.pop(ident, None)
|
||||
if old:
|
||||
events.append((old['start'], es, None, 'FIRST replaced incomplete packet'))
|
||||
if len(self.pending) >= 64:
|
||||
raise ValueError('Too many concurrent SET CAN packets (64)')
|
||||
self.pending[ident] = dict(start=ss, time=now_ms, total=number(data[1:3]),
|
||||
data=bytearray(data[3:]), sequence=1)
|
||||
events.append((ss, es, dict(protocol='SET CAN', summary='FIRST length=%d' % number(data[1:3])), None))
|
||||
elif pci == 0x20:
|
||||
state = self.pending.get(ident)
|
||||
if not state or len(data) < 2 or data[0] & 15 != state['sequence']:
|
||||
raise ValueError('Unexpected CONSECUTIVE sequence')
|
||||
state['data'].extend(data[1:])
|
||||
state['time'], state['sequence'] = now_ms, (state['sequence'] + 1) & 15
|
||||
if len(state['data']) > state['total']:
|
||||
raise ValueError('SET CAN packet exceeds advertised length')
|
||||
if len(state['data']) == state['total']:
|
||||
packet = parse_frame(state['data'])
|
||||
if packet['protocol'] != 'SET v2' or packet['source'] != (ident >> 8) & 255 or packet['destination'] != (ident >> 16) & 255 or bool(packet['flags'] & 32) != bool(ident & 128):
|
||||
raise ValueError('CAN ID and SET header disagree')
|
||||
events.append((state['start'], es, packet, None))
|
||||
del self.pending[ident]
|
||||
else:
|
||||
events.append((ss, es, dict(protocol='SET CAN', summary='CONSECUTIVE SN=%d' % (data[0] & 15)), None))
|
||||
else:
|
||||
raise ValueError('Unknown SET CAN PCI')
|
||||
except ValueError as exc:
|
||||
state = self.pending.pop(ident, None)
|
||||
events.append((state['start'] if state else ss, es, None, str(exc)))
|
||||
return events
|
||||
133
python/logic_analyzer/decoders/set_uart.py
Normal file
133
python/logic_analyzer/decoders/set_uart.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Portable wire readers based on templates/c/set-protocol (no native DLL)."""
|
||||
import binascii
|
||||
|
||||
|
||||
V2 = {1: 'PING', 2: 'DEVICE_INFO', 3: 'CAPABILITIES', 8: 'DIAGNOSTICS',
|
||||
9: 'READ', 10: 'WRITE', 16: 'LOG_READ', 17: 'CATALOG',
|
||||
18: 'SUBSCRIBE', 19: 'PUBLISH', 20: 'UNSUBSCRIBE'}
|
||||
V2.update(dict((256 + i, 'FW_' + name) for i, name in enumerate(
|
||||
('BEGIN', 'DATA', 'END', 'ABORT', 'STATUS', 'ACTIVATE'))))
|
||||
V1 = dict(enumerate(('PING DEVICE_INFO GET_OBJECT SET_OBJECT GET_OBJECT_LIST '
|
||||
'GET_OBJECT_INFO COMMAND_STATUS DIAGNOSTICS READ_REGISTERS WRITE_REGISTERS '
|
||||
'FIRMWARE_BEGIN FIRMWARE_DATA FIRMWARE_END FIRMWARE_ABORT FIRMWARE_STATUS '
|
||||
'READ_LOGS GAS_CATALOG GAS_WATCH_SET GAS_WATCH_DATA').split(), 1))
|
||||
V1.update({128: 'NACK', 129: 'ACK', 130: 'ERROR'})
|
||||
V1.update(dict(enumerate(('SENSOR_SCAN SENSOR_LIST SENSOR_READ SENSOR_DATA '
|
||||
'SET_USER_BYTES SET_RESOLUTION SET_POLL_PERIOD SEND_ID_CAN UI_KEY UI_READ '
|
||||
'UI_STATE EEPROM_SCAN EEPROM_INFO EEPROM_READ EEPROM_LIST').split(), 32)))
|
||||
|
||||
|
||||
def number(data, order='little'):
|
||||
return int.from_bytes(bytes(data), order)
|
||||
|
||||
|
||||
def crc16(data, modbus=False):
|
||||
crc = 0xffff
|
||||
for byte in data:
|
||||
crc ^= byte if modbus else byte << 8
|
||||
for _ in range(8):
|
||||
if modbus:
|
||||
crc = (crc >> 1) ^ (0xa001 if crc & 1 else 0)
|
||||
else:
|
||||
crc = ((crc << 1) ^ (0x1021 if crc & 0x8000 else 0)) & 0xffff
|
||||
return crc
|
||||
|
||||
|
||||
def parse_frame(raw):
|
||||
raw = bytes(raw)
|
||||
bridge = raw[:2] == b'\xaa\x55'
|
||||
if bridge:
|
||||
if len(raw) < 11 or not 6 <= raw[2] <= 14 or len(raw) != raw[2] + 5:
|
||||
raise ValueError('Invalid ProtoCAN length')
|
||||
crc, received = crc16(raw[2:-2]), number(raw[-2:])
|
||||
ident, flags = number(raw[5:9]), raw[4]
|
||||
if ident > (0x1fffffff if flags & 1 else 0x7ff) or flags & 0xf0:
|
||||
raise ValueError('Invalid ProtoCAN ID/flags')
|
||||
result = dict(protocol='ProtoCAN bridge', sequence=raw[3], flags=flags,
|
||||
can_id=ident, payload=raw[9:-2])
|
||||
result['summary'] = 'ProtoCAN seq=%d ID=%08X flags=%02X' % (raw[3], ident, flags)
|
||||
else:
|
||||
if len(raw) < 12 or raw[:2] != b'\xa5\x5a' or raw[2] not in (1, 2):
|
||||
raise ValueError('Invalid SET signature/version')
|
||||
version = raw[2]
|
||||
header = 14 if version == 2 else 8
|
||||
if len(raw) < header + 4:
|
||||
raise ValueError('Incomplete SET header')
|
||||
size = number(raw[12:14]) if version == 2 else number(raw[6:8], 'big')
|
||||
if size > 512 or len(raw) != header + size + 4:
|
||||
raise ValueError('Invalid SET payload length')
|
||||
crc, received = binascii.crc32(raw[2:-4]) & 0xffffffff, number(raw[-4:])
|
||||
msg = number(raw[4:6]) if version == 2 else raw[3]
|
||||
seq = number(raw[10:12]) if version == 2 else number(raw[4:6], 'big')
|
||||
result = dict(protocol='SET v2' if version == 2 else 'SETGUI v1',
|
||||
message_type=msg, sequence=seq, payload=raw[header:-4])
|
||||
name = (V2 if version == 2 else V1).get(msg, 'TYPE_%04X' % msg)
|
||||
result['summary'] = '%s %s seq=%d len=%d' % (result['protocol'], name, seq, size)
|
||||
if version == 2:
|
||||
result.update(source=number(raw[6:8]), destination=number(raw[8:10]), flags=raw[3])
|
||||
result['summary'] += ' %04X->%04X flags=%02X' % (result['source'], result['destination'], raw[3])
|
||||
if raw[3] & 0xc0:
|
||||
raise ValueError('Reserved SET flags')
|
||||
if raw[3] & 1:
|
||||
if size < 2:
|
||||
raise ValueError('SET response missing status')
|
||||
result['status'] = number(result['payload'][:2])
|
||||
result['summary'] += ' status=%d' % result['status']
|
||||
if crc != received:
|
||||
raise ValueError('CRC mismatch: received %X expected %X' % (received, crc))
|
||||
result['raw'] = raw
|
||||
return result
|
||||
|
||||
|
||||
class StreamParser:
|
||||
"""Bounded byte stream; keep sample positions during resynchronization."""
|
||||
def __init__(self, mode='auto'):
|
||||
self.mode = mode
|
||||
self.buffer = []
|
||||
|
||||
def feed(self, value, start, end):
|
||||
self.buffer.append((value, start, end))
|
||||
events = []
|
||||
while len(self.buffer) >= 3:
|
||||
raw = bytes(item[0] for item in self.buffer)
|
||||
bridge = raw[:2] == b'\xaa\x55' and self.mode in ('auto', 'bridge')
|
||||
modern = raw[:2] == b'\xa5\x5a' and self.mode != 'bridge'
|
||||
if not bridge and not modern:
|
||||
del self.buffer[0]
|
||||
continue
|
||||
error = None
|
||||
if bridge:
|
||||
total = raw[2] + 5
|
||||
if not 6 <= raw[2] <= 14:
|
||||
error = 'Invalid ProtoCAN length'
|
||||
else:
|
||||
if raw[2] not in (1, 2) or self.mode in ('v1', 'v2') and raw[2] != int(self.mode[1]):
|
||||
error = 'Unexpected SET version'
|
||||
header = 14 if raw[2] == 2 else 8
|
||||
if not error and len(raw) < header:
|
||||
break
|
||||
size = number(raw[12:14]) if raw[2] == 2 else number(raw[6:8], 'big')
|
||||
total = header + size + 4
|
||||
if size > 512:
|
||||
error = 'Payload exceeds 512 bytes'
|
||||
if not error and len(raw) < total:
|
||||
break
|
||||
last = min(len(raw), total) - 1
|
||||
result = None
|
||||
if not error:
|
||||
try:
|
||||
result = parse_frame(raw[:total])
|
||||
except ValueError as exc:
|
||||
error = str(exc)
|
||||
events.append((self.buffer[0][1], self.buffer[last][2], result, error))
|
||||
del self.buffer[:1 if error else total]
|
||||
return events
|
||||
|
||||
def flush(self):
|
||||
if not self.buffer:
|
||||
return []
|
||||
pending = self.buffer
|
||||
self.buffer = []
|
||||
if pending[0][0] not in (0xa5, 0xaa):
|
||||
return []
|
||||
return [(pending[0][1], pending[-1][2], None, 'Incomplete frame')]
|
||||
299
python/logic_analyzer/files.py
Normal file
299
python/logic_analyzer/files.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""Digital capture readers. Store edges, never expand captures to sample arrays.
|
||||
|
||||
DSView layout: DreamSourceLab/DSView libsigrok4DSL/session_driver.c.
|
||||
SAL v1 layout is validated against example/digitOSC; v3 RLE layout reference:
|
||||
https://github.com/nemanjan00/sigrok2sal/blob/master/SAL_FORMAT.md
|
||||
Unknown binary versions fail explicitly instead of guessing their layout.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from array import array
|
||||
from bisect import bisect_right
|
||||
import configparser
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import zipfile
|
||||
|
||||
|
||||
class ImportCancelled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DigitalChannel:
|
||||
name: str
|
||||
initial: int
|
||||
edges: array
|
||||
|
||||
def level_at(self, time):
|
||||
return self.initial ^ (bisect_right(self.edges, time) & 1)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DigitalCapture:
|
||||
source: str
|
||||
format: str
|
||||
channels: list
|
||||
start: float
|
||||
end: float
|
||||
sample_rate: float = 0
|
||||
|
||||
|
||||
def _check(cancel):
|
||||
if cancel():
|
||||
raise ImportCancelled()
|
||||
|
||||
|
||||
def read_capture(path, progress=lambda value: None, cancel=lambda: False):
|
||||
path = Path(path)
|
||||
try:
|
||||
if path.suffix.lower() == '.csv':
|
||||
result = _csv(path, progress, cancel)
|
||||
elif path.suffix.lower() in ('.sal', '.dsl'):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
result = (_sal if path.suffix.lower() == '.sal' else _dsl)(
|
||||
path, archive, progress, cancel)
|
||||
else:
|
||||
raise ValueError('Выберите цифровую запись CSV, SAL или DSL.')
|
||||
except (KeyError, struct.error, zipfile.BadZipFile, EOFError,
|
||||
configparser.Error, UnicodeError) as exc:
|
||||
raise ValueError('Повреждённый или неподдерживаемый файл: %s' % exc) from exc
|
||||
_check(cancel)
|
||||
if not result.channels or not math.isfinite(result.start) or not math.isfinite(result.end):
|
||||
raise ValueError('Файл не содержит цифровой записи.')
|
||||
progress(100)
|
||||
return result
|
||||
|
||||
|
||||
def _csv(path, progress, cancel):
|
||||
size = max(1, path.stat().st_size)
|
||||
consumed = 0
|
||||
channels = None
|
||||
previous = []
|
||||
start = end = None
|
||||
dsview = False
|
||||
with path.open(encoding='utf-8-sig', newline='') as stream:
|
||||
for number, line in enumerate(stream, 1):
|
||||
consumed += len(line)
|
||||
if number % 4096 == 0:
|
||||
_check(cancel)
|
||||
progress(min(99, int(consumed * 100 / size)))
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.lstrip().startswith((';', '#')):
|
||||
dsview |= 'DSView' in line
|
||||
continue
|
||||
if channels is None:
|
||||
delimiter = max((',', ';', '\t'), key=line.count)
|
||||
names = [v.strip() for v in next(csv.reader([line], delimiter=delimiter))]
|
||||
match = re.fullmatch(r'(?:time|timestamp)\s*(?:[\[(](s|ms|us|µs|ns)[\])])?', names[0], re.I)
|
||||
if not match or not 2 <= len(names) <= 65 or any(not n for n in names):
|
||||
raise ValueError('Ожидается CSV: Time [s], затем цифровые каналы (0/1).')
|
||||
if len(set(names)) != len(names):
|
||||
raise ValueError('Повторяющиеся имена каналов CSV.')
|
||||
factor = {'s': 1, 'ms': 1e-3, 'us': 1e-6, 'µs': 1e-6, 'ns': 1e-9}[(match[1] or 's').lower()]
|
||||
channels = [DigitalChannel(name, 0, array('d')) for name in names[1:]]
|
||||
continue
|
||||
row = next(csv.reader([line], delimiter=delimiter))
|
||||
try:
|
||||
time = float(row[0].strip().replace(',', '.')) * factor
|
||||
values = [v.strip() for v in row[1:]]
|
||||
if len(values) != len(channels) or any(v not in ('0', '1') for v in values):
|
||||
raise ValueError('цифровые уровни должны быть 0 или 1')
|
||||
if not math.isfinite(time) or (end is not None and time <= end):
|
||||
raise ValueError('время должно строго возрастать и быть конечным')
|
||||
except (ValueError, IndexError) as exc:
|
||||
raise ValueError('CSV, строка %d: %s' % (number, exc)) from exc
|
||||
if start is None:
|
||||
start = time
|
||||
for channel, value in zip(channels, values):
|
||||
channel.initial = int(value)
|
||||
else:
|
||||
for i, channel in enumerate(channels):
|
||||
if values[i] != previous[i]:
|
||||
channel.edges.append(time)
|
||||
previous = values
|
||||
end = time
|
||||
if start is None:
|
||||
raise ValueError('CSV не содержит данных.')
|
||||
return DigitalCapture(str(path), 'DSLogic CSV' if dsview else 'Logic 2 CSV', channels, start, end)
|
||||
|
||||
|
||||
class _Binary:
|
||||
def __init__(self, data):
|
||||
self.data, self.pos = data, 0
|
||||
|
||||
def take(self, size):
|
||||
if size < 0 or self.pos + size > len(self.data):
|
||||
raise ValueError('Обрезанный двоичный блок.')
|
||||
value = self.data[self.pos:self.pos + size]
|
||||
self.pos += size
|
||||
return value
|
||||
|
||||
def unpack(self, fmt):
|
||||
return struct.unpack('<' + fmt, self.take(struct.calcsize('<' + fmt)))
|
||||
|
||||
|
||||
def _runs(data):
|
||||
pos = 0
|
||||
while pos < len(data):
|
||||
value = data[pos]
|
||||
pos += 1
|
||||
if value >= 128:
|
||||
raise ValueError('Некорректный RLE SAL.')
|
||||
if value & 64:
|
||||
value &= 63
|
||||
for _ in range(9):
|
||||
if pos == len(data):
|
||||
raise ValueError('Обрезанный RLE SAL.')
|
||||
byte = data[pos]
|
||||
pos += 1
|
||||
value = (value << 7) | (byte & 127)
|
||||
if not byte & 128:
|
||||
break
|
||||
else:
|
||||
raise ValueError('Слишком длинный RLE SAL.')
|
||||
yield value + 1
|
||||
|
||||
|
||||
def _sal_channel(data, name, cancel):
|
||||
reader = _Binary(data)
|
||||
if reader.take(8) != b'<SALEAE>':
|
||||
raise ValueError('Неверная сигнатура SAL.')
|
||||
version, kind = reader.unpack('II')
|
||||
if version not in (1, 3, 4) or kind != 100:
|
||||
raise ValueError('Не поддерживается цифровой SAL версии %d, тип %d.' % (version, kind))
|
||||
schema, rate, unix_ms, fractional_ms = reader.unpack('BdQd')
|
||||
if schema != 1 or not math.isfinite(rate) or rate <= 0:
|
||||
raise ValueError('Некорректный заголовок канала SAL.')
|
||||
for _ in range(2):
|
||||
optional, = reader.unpack('B')
|
||||
if optional not in (0, 1):
|
||||
raise ValueError('Неизвестный заголовок SAL.')
|
||||
if optional:
|
||||
reader.take(8)
|
||||
count, = reader.unpack('Q')
|
||||
if not 0 < count <= len(data) // 26:
|
||||
raise ValueError('Неверное число блоков SAL.')
|
||||
channel = DigitalChannel(name, 0, array('d'))
|
||||
previous_end = None
|
||||
level = 0
|
||||
for chunk in range(count):
|
||||
_check(cancel)
|
||||
if version == 1:
|
||||
begin, end, samples, chunk_rate, scale, length = reader.unpack('6Q')
|
||||
if samples != end - begin or chunk_rate != rate or scale != 1:
|
||||
raise ValueError('Неподдерживаемая шкала блока SAL.')
|
||||
encoded = reader.take(length)
|
||||
entries, = reader.unpack('Q')
|
||||
if not entries:
|
||||
raise ValueError('В SAL отсутствует индекс начального уровня.')
|
||||
index = reader.take(entries * 20)
|
||||
offset, byte_offset, initial = struct.unpack_from('<QQI', index)
|
||||
if offset or byte_offset:
|
||||
raise ValueError('Некорректный индекс SAL.')
|
||||
else:
|
||||
begin, end, initial, flags, length = reader.unpack('QQBBQ')
|
||||
if flags:
|
||||
raise ValueError('Неизвестные флаги SAL.')
|
||||
encoded = reader.take(length)
|
||||
if initial not in (0, 1) or end <= begin or (previous_end is not None and begin != previous_end):
|
||||
raise ValueError('Некорректные границы или уровень блока SAL.')
|
||||
if chunk == 0:
|
||||
first = begin
|
||||
channel.initial = initial
|
||||
elif initial != level:
|
||||
channel.edges.append(begin / rate)
|
||||
level = initial
|
||||
position = begin
|
||||
for run_index, run in enumerate(_runs(encoded)):
|
||||
if run_index % 8192 == 0:
|
||||
_check(cancel)
|
||||
position += run
|
||||
if position > end:
|
||||
raise ValueError('RLE выходит за границы блока SAL.')
|
||||
if position < end:
|
||||
channel.edges.append(position / rate)
|
||||
level ^= 1
|
||||
if position != end:
|
||||
raise ValueError('Неполный RLE блока SAL.')
|
||||
previous_end = end
|
||||
if reader.pos != len(data):
|
||||
raise ValueError('Неизвестные дополнительные данные SAL.')
|
||||
return channel, first / rate, previous_end / rate, rate
|
||||
|
||||
|
||||
def _sal(path, archive, progress, cancel):
|
||||
meta = json.loads(archive.read('meta.json'))
|
||||
names = {}
|
||||
for row in meta.get('data', {}).get('rowsSettings', []):
|
||||
channel = row.get('channel', {})
|
||||
if channel.get('type') == 'Digital':
|
||||
names[channel.get('deviceChannel')] = row.get('name')
|
||||
files = sorted((int(m[1]), name) for name in archive.namelist()
|
||||
for m in [re.fullmatch(r'digital-(\d+)\.bin', name)] if m)
|
||||
if not files:
|
||||
raise ValueError('В SAL нет цифровых каналов.')
|
||||
channels, bounds = [], []
|
||||
for i, (index, filename) in enumerate(files):
|
||||
_check(cancel)
|
||||
channel, start, end, rate = _sal_channel(archive.read(filename), names.get(index) or 'D%d' % index, cancel)
|
||||
channels.append(channel)
|
||||
bounds.append((start, end, rate))
|
||||
progress(int((i + 1) * 100 / len(files)))
|
||||
if any(bound != bounds[0] for bound in bounds):
|
||||
raise ValueError('SAL с разными временными границами каналов не поддерживается.')
|
||||
return DigitalCapture(str(path), 'Logic 2 SAL', channels, *bounds[0])
|
||||
|
||||
|
||||
def _dsl(path, archive, progress, cancel):
|
||||
config = configparser.ConfigParser(interpolation=None)
|
||||
config.read_string(archive.read('header').decode('utf-8-sig'))
|
||||
version = config.getint('version', 'version')
|
||||
header = config['header']
|
||||
if version not in (2, 3) or header.getint('device mode', 0) != 0:
|
||||
raise ValueError('Поддерживаются цифровые сессии DSView DSL версий 2 и 3.')
|
||||
match = re.fullmatch(r'\s*([\d.]+)\s*([kKmMgG]?)(?:Hz)?\s*', header['samplerate'])
|
||||
if not match:
|
||||
raise ValueError('Неверная частота DSL.')
|
||||
rate = float(match[1]) * {'': 1, 'k': 1e3, 'm': 1e6, 'g': 1e9}[match[2].lower()]
|
||||
samples = header.getint('total samples')
|
||||
blocks = header.getint('total blocks')
|
||||
if not math.isfinite(rate) or rate <= 0 or samples <= 0 or blocks <= 0:
|
||||
raise ValueError('Некорректный размер или частота DSL.')
|
||||
probes = sorted((int(m[1]), name) for key, name in header.items()
|
||||
for m in [re.fullmatch(r'probe(\d+)', key)] if m)
|
||||
channels = []
|
||||
for i, (index, name) in enumerate(probes):
|
||||
channel = DigitalChannel(name, 0, array('d'))
|
||||
position, level = 0, 0
|
||||
for block in range(blocks):
|
||||
_check(cancel)
|
||||
# DSView v3 stores enabled channels in sequential directories.
|
||||
directory = i if version == 3 else index
|
||||
data = archive.read('L-%d/%d' % (directory, block))
|
||||
for offset, byte in enumerate(data):
|
||||
if offset % 65536 == 0:
|
||||
_check(cancel)
|
||||
if position >= samples:
|
||||
break
|
||||
if position == 0:
|
||||
channel.initial = level = byte & 1
|
||||
changes = byte ^ ((byte << 1) & 255 | level)
|
||||
while changes:
|
||||
bit = (changes & -changes).bit_length() - 1
|
||||
if position + bit < samples:
|
||||
channel.edges.append((position + bit) / rate)
|
||||
changes &= changes - 1
|
||||
level = byte >> 7
|
||||
position += 8
|
||||
if position < samples:
|
||||
raise ValueError('Обрезанные данные DSL, канал %s.' % name)
|
||||
channels.append(channel)
|
||||
progress(int((i + 1) * 100 / max(1, len(probes))))
|
||||
return DigitalCapture(str(path), 'DSLogic DSL', channels, 0, samples / rate, rate)
|
||||
155
python/logic_analyzer/saleae.py
Normal file
155
python/logic_analyzer/saleae.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Local Logic 2 acquisition through the optional Saleae Automation API."""
|
||||
from __future__ import annotations
|
||||
|
||||
from array import array
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from logic_analyzer.files import DigitalCapture, DigitalChannel, ImportCancelled
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SaleaeSettings:
|
||||
device_id: str
|
||||
channels: tuple = (0, 1)
|
||||
sample_rate: int = 24000000
|
||||
duration: float = 1.0 # Wall-clock stop timer; zero means manual stop.
|
||||
threshold: float = None
|
||||
buffer_mb: int = 256
|
||||
port: int = 10430
|
||||
|
||||
def validate(self):
|
||||
if not self.device_id:
|
||||
raise ValueError('Выберите устройство Saleae.')
|
||||
if (not self.channels or len(set(self.channels)) != len(self.channels)
|
||||
or any(type(c) is not int or not 0 <= c <= 15 for c in self.channels)):
|
||||
raise ValueError('Выберите цифровые каналы D0…D15 без повторений.')
|
||||
if not 1 <= self.sample_rate <= 500000000:
|
||||
raise ValueError('Частота должна быть от 1 до 500000000 выборок/с.')
|
||||
if not math.isfinite(self.duration) or not 0 <= self.duration <= 3600:
|
||||
raise ValueError('Таймер должен быть от 0 до 3600 с.')
|
||||
if self.threshold not in (None, 1.2, 1.8, 3.3):
|
||||
raise ValueError('Неподдерживаемый порог цифрового входа.')
|
||||
if not 16 <= self.buffer_mb <= 4096 or not 1 <= self.port <= 65535:
|
||||
raise ValueError('Некорректный буфер или порт Automation API.')
|
||||
|
||||
|
||||
def automation_api():
|
||||
try:
|
||||
from saleae import automation
|
||||
except ImportError as exc:
|
||||
raise RuntimeError('Не установлен logic2-automation. Для запуска из Python: '
|
||||
'pip install logic2-automation; для EXE нужна сборка с этим пакетом.') from exc
|
||||
return automation
|
||||
|
||||
|
||||
def connect(api, port):
|
||||
# Bound every RPC, including stop/export/close, when Logic 2 stops responding.
|
||||
config = json.dumps({'methodConfig': [{'name': [{}], 'timeout': '30s'}]})
|
||||
try:
|
||||
return api.Manager.connect(port=port, connect_timeout_seconds=3,
|
||||
grpc_channel_arguments=[('grpc.service_config', config)])
|
||||
except Exception as exc:
|
||||
raise RuntimeError('Нет связи с Logic 2 на localhost:%d. Запустите Logic 2 и '
|
||||
'включите Preferences → Enable Automation Server. %s' % (port, exc)) from exc
|
||||
|
||||
|
||||
def list_devices(port=10430, simulation=False):
|
||||
api = automation_api()
|
||||
with connect(api, port) as manager:
|
||||
return manager.get_devices(include_simulation_devices=simulation)
|
||||
|
||||
|
||||
def read_binary(directory, channels, cancel=lambda: False):
|
||||
"""Read documented Saleae digital binary v0, preserving constant tails."""
|
||||
result, bounds = [], None
|
||||
header = struct.Struct('<8siiIddQ')
|
||||
for channel in channels:
|
||||
path = Path(directory) / ('digital_%d.bin' % channel)
|
||||
with path.open('rb') as stream:
|
||||
raw = stream.read(header.size)
|
||||
if len(raw) != header.size:
|
||||
raise ValueError('Обрезанный заголовок Saleae: ' + path.name)
|
||||
magic, version, kind, initial, start, end, count = header.unpack(raw)
|
||||
if magic != b'<SALEAE>' or version != 0 or kind != 0:
|
||||
raise ValueError('Неподдерживаемый бинарный экспорт Saleae (нужен digital v0).')
|
||||
if (initial not in (0, 1) or not math.isfinite(start) or not math.isfinite(end)
|
||||
or end < start or path.stat().st_size != header.size + count * 8):
|
||||
raise ValueError('Повреждённый бинарный экспорт Saleae.')
|
||||
if bounds is not None and bounds != (start, end):
|
||||
raise ValueError('Границы каналов Saleae не совпадают.')
|
||||
bounds = start, end
|
||||
edges = array('d')
|
||||
previous = start
|
||||
while count:
|
||||
if cancel():
|
||||
raise ImportCancelled()
|
||||
size = min(count, 65536)
|
||||
block = array('d')
|
||||
block.frombytes(stream.read(size * 8))
|
||||
if sys.byteorder != 'little':
|
||||
block.byteswap()
|
||||
for edge in block:
|
||||
if not math.isfinite(edge) or edge < previous or edge > end:
|
||||
raise ValueError('Некорректное время фронта Saleae.')
|
||||
previous = edge
|
||||
edges.extend(block)
|
||||
count -= size
|
||||
result.append(DigitalChannel('D%d' % channel, initial, edges))
|
||||
if not result:
|
||||
raise ValueError('Нет цифровых каналов Saleae.')
|
||||
return DigitalCapture('', 'Saleae ONLINE', result, *bounds)
|
||||
|
||||
|
||||
def acquire(settings, stop, cancel, status=lambda text: None):
|
||||
"""Stop exactly once; cancel discards data, stop imports the captured window."""
|
||||
settings.validate()
|
||||
api = automation_api()
|
||||
status('Подключение к Logic 2…')
|
||||
with connect(api, settings.port) as manager:
|
||||
if cancel():
|
||||
raise ImportCancelled()
|
||||
device = api.LogicDeviceConfiguration(
|
||||
enabled_digital_channels=list(settings.channels),
|
||||
digital_sample_rate=settings.sample_rate,
|
||||
digital_threshold_volts=settings.threshold)
|
||||
configuration = api.CaptureConfiguration(buffer_size_megabytes=settings.buffer_mb,
|
||||
capture_mode=api.ManualCaptureMode())
|
||||
capture = manager.start_capture(device_id=settings.device_id,
|
||||
device_configuration=device,
|
||||
capture_configuration=configuration)
|
||||
stopped = False
|
||||
try:
|
||||
status('Захват Saleae… Нажмите «Стоп и показать» для завершения.')
|
||||
deadline = time.monotonic() + settings.duration if settings.duration else None
|
||||
while not stop() and not cancel():
|
||||
if deadline is not None and time.monotonic() >= deadline:
|
||||
break
|
||||
time.sleep(.02)
|
||||
stopped = True # Never retry stop(), even if it raises.
|
||||
capture.stop()
|
||||
if cancel():
|
||||
raise ImportCancelled()
|
||||
status('Получение цифровых каналов…')
|
||||
with tempfile.TemporaryDirectory(prefix='setgui-saleae-') as directory:
|
||||
capture.export_raw_data_binary(directory, digital_channels=list(settings.channels),
|
||||
analog_channels=[])
|
||||
result = read_binary(directory, settings.channels, cancel)
|
||||
if cancel():
|
||||
raise ImportCancelled()
|
||||
result.source = 'Saleae %s · %g MS/s' % (settings.device_id, settings.sample_rate / 1e6)
|
||||
result.sample_rate = settings.sample_rate
|
||||
return result
|
||||
finally:
|
||||
if not stopped:
|
||||
with suppress(Exception):
|
||||
capture.stop()
|
||||
with suppress(Exception):
|
||||
capture.close()
|
||||
@@ -1,5 +1,10 @@
|
||||
"""Переносимые модули и тонкая Python-обёртка SETProtocol."""
|
||||
|
||||
# Consumers may supply additional platform ports from their pinned templates
|
||||
# checkout. The selected primary checkout wins; missing modules can coexist.
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
|
||||
from .native import (
|
||||
NativeCore, NativeCoreUnavailable, NativeFrame, NativeGuiFrame,
|
||||
NativeGuiParser, NativeParser, NativeProtocol, NativeProtocolUnavailable,
|
||||
|
||||
123
python/protocan/can_boot.py
Normal file
123
python/protocan/can_boot.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Legacy ProtoCAN Boot client ported from Gui_Android CanFirmwareProtocol.kt."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
|
||||
from .protocan import ProtoCanId
|
||||
from .transport import build_frame
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanBootTarget:
|
||||
device_type: int = 0
|
||||
device: int = 13
|
||||
product_type: int = 0x166
|
||||
hardware_min: int = 0
|
||||
hardware_max: int = 255
|
||||
session_id: int = 1
|
||||
|
||||
def __post_init__(self):
|
||||
if not 0 <= self.device_type <= 7 or not 0 <= self.device <= 15:
|
||||
raise ValueError("Адрес ProtoCAN: Device Type 0…7, Device 0…15")
|
||||
if not 0 <= self.product_type <= 65535:
|
||||
raise ValueError("Product Type вне диапазона u16")
|
||||
if not 0 <= self.hardware_min <= self.hardware_max <= 255:
|
||||
raise ValueError("Неверный диапазон аппаратных ревизий")
|
||||
if not 1 <= self.session_id <= 255:
|
||||
raise ValueError("Session ID должен быть 1…255")
|
||||
|
||||
|
||||
class CanBootTransfer:
|
||||
"""Windowed 8-byte blocks; control 0x9, slots 0xA/0xB, status 0xC."""
|
||||
def __init__(self, image, target: CanBootTarget):
|
||||
if not 0 < len(image.data) <= 512 * 1024:
|
||||
raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ")
|
||||
if not 0 <= image.version <= 0xFFFFFFFF:
|
||||
raise ValueError("Версия образа вне диапазона u32")
|
||||
self.image, self.target = image, target
|
||||
self.stage = "enter"
|
||||
self.next_block = 0
|
||||
self.slot = None
|
||||
self.finished = False
|
||||
self._window_end = 0
|
||||
|
||||
@property
|
||||
def percent(self):
|
||||
return min(100, self.next_block * 8 * 100 // len(self.image.data))
|
||||
|
||||
def _frame(self, kind, body, data=b""):
|
||||
t = self.target
|
||||
return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, kind, body), data, to_can=True)
|
||||
|
||||
def _control(self, command, data=b""):
|
||||
return [self._frame(9, self.target.session_id << 8 | command, data)]
|
||||
|
||||
def start(self):
|
||||
return self._control(2)
|
||||
|
||||
def abort(self):
|
||||
return self._control(10)
|
||||
|
||||
def accepts(self, frame):
|
||||
p = ProtoCanId.parse(frame.can_id)
|
||||
t = self.target
|
||||
return (not frame.to_can and frame.ide and not frame.rtr and not frame.is_error
|
||||
and p.msg_type == 12 and p.pm == 1 and p.device_type == t.device_type
|
||||
and p.device == t.device and p.body >> 8 == t.session_id and len(frame.data) == 8)
|
||||
|
||||
def _window(self):
|
||||
total = (len(self.image.data) + 7) // 8
|
||||
self._window_end = min(total, self.next_block + 16)
|
||||
return [self._frame(10 + self.slot, i, self.image.data[i * 8:(i + 1) * 8].ljust(8, b"\xff"))
|
||||
for i in range(self.next_block, self._window_end)]
|
||||
|
||||
def handle_status(self, frame):
|
||||
if not self.accepts(frame):
|
||||
return [], ""
|
||||
command = ProtoCanId.parse(frame.can_id).body & 255
|
||||
expected_command = {"enter": 2, "image": 3, "compat": 4, "erase": 5,
|
||||
"data": 0, "verify": 6, "commit": 7, "reboot": 9}.get(self.stage)
|
||||
if command != expected_command:
|
||||
return [], ""
|
||||
status, slot, expected = struct.unpack_from("<BBH", frame.data)
|
||||
if status == 1:
|
||||
return [], "Загрузчик занят"
|
||||
if status not in (0, 8) or (status == 8 and self.stage != "data"):
|
||||
raise RuntimeError(f"ProtoCAN Boot: ошибка 0x{status:02X}")
|
||||
if self.stage == "enter":
|
||||
self.stage = "image"
|
||||
return self._control(3, struct.pack("<II", len(self.image.data), self.image.crc32)), "Передача метаданных"
|
||||
if self.stage == "image":
|
||||
self.stage = "compat"
|
||||
t = self.target
|
||||
return self._control(4, struct.pack("<HBBI", t.product_type, t.hardware_min, t.hardware_max, self.image.version)), "Проверка совместимости"
|
||||
if self.stage == "compat":
|
||||
if slot not in (0, 1):
|
||||
raise RuntimeError("ProtoCAN Boot: неверный слот")
|
||||
self.slot, self.stage = slot, "erase"
|
||||
return self._control(5), "Стирание неактивного слота"
|
||||
if self.stage in ("erase", "data"):
|
||||
total = (len(self.image.data) + 7) // 8
|
||||
# The final index of a 512 KiB image wraps the u16 status field.
|
||||
if self.stage == "data" and expected == 0 and self._window_end == 65536 and status == 0:
|
||||
expected = 65536
|
||||
limit = total if self.stage == "erase" else self._window_end
|
||||
if expected > limit:
|
||||
raise RuntimeError("ProtoCAN Boot: подтверждён непереданный блок")
|
||||
if self.stage == "data" and status == 0 and expected < self.next_block:
|
||||
return [], ""
|
||||
self.next_block = expected
|
||||
if expected == total:
|
||||
self.stage = "verify"
|
||||
return self._control(6), "Проверка CRC32"
|
||||
self.stage = "data"
|
||||
return self._window(), "Передача блоков ProtoCAN Boot"
|
||||
if self.stage == "verify":
|
||||
self.stage = "commit"
|
||||
return self._control(7), "Активация образа"
|
||||
if self.stage == "commit":
|
||||
self.stage = "reboot"
|
||||
return self._control(9), "Перезапуск прибора"
|
||||
self.stage, self.finished = "finished", True
|
||||
return [], "Прошивка ProtoCAN Boot завершена"
|
||||
@@ -131,9 +131,11 @@ MSGTYPE_RU = {
|
||||
#: но не присваивает приборам имён.
|
||||
DEVICE_TYPE_NAMES = {
|
||||
0x0: "Верхний уровень",
|
||||
0x6: "Логические анализаторы",
|
||||
}
|
||||
|
||||
DEVICE_ADDRESS_NAMES = {
|
||||
(0x6, 0xE): "Altera Logic",
|
||||
(0x7, 0xD): "configurator",
|
||||
}
|
||||
|
||||
|
||||
239
python/protocan/setp_firmware.py
Normal file
239
python/protocan/setp_firmware.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""SETProtocol v2 firmware client over segmented classic CAN."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from time import perf_counter
|
||||
|
||||
from setprotocol.can import CanAddress, CanFrame, CanReassembler, segment
|
||||
from setprotocol.core import (
|
||||
Capabilities,
|
||||
DeviceInfo,
|
||||
Feature,
|
||||
FirmwareBegin,
|
||||
FirmwareFlag,
|
||||
Frame as SetFrame,
|
||||
FrameFlag,
|
||||
MessageType,
|
||||
SetProtocolError,
|
||||
Status,
|
||||
build_frame,
|
||||
decode_datagram,
|
||||
decode_response,
|
||||
encode_firmware_data,
|
||||
)
|
||||
|
||||
from . import transport as tr
|
||||
from typing import Any
|
||||
|
||||
FirmwareImage = Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanFirmwareTarget:
|
||||
"""Address and compatibility settings for a SETProtocol v2 target."""
|
||||
|
||||
node_id: int
|
||||
device_class: int
|
||||
hardware_min: int = 0
|
||||
hardware_max: int = 0xFF
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 0 <= self.node_id <= 0xFF:
|
||||
raise ValueError("SETP node ID вне диапазона 0..255")
|
||||
if not 0 <= self.device_class <= 0xFFFF:
|
||||
raise ValueError("Device class вне диапазона u16")
|
||||
if not 0 <= self.hardware_min <= self.hardware_max <= 0xFFFFFFFF:
|
||||
raise ValueError("Диапазон hardware version задан неверно")
|
||||
|
||||
|
||||
class CanFirmwareTransfer:
|
||||
"""Stop-and-wait SETP v2 firmware transaction for a classic CAN link."""
|
||||
|
||||
HOST_NODE = 0
|
||||
CHANNEL = 1
|
||||
DEFAULT_BLOCK_SIZE = 64
|
||||
|
||||
def __init__(self, image: FirmwareImage, target: CanFirmwareTarget) -> None:
|
||||
if not image.data:
|
||||
raise ValueError("Образ прошивки пуст")
|
||||
if len(image.data) > 512 * 1024:
|
||||
raise ValueError("BALZAM поддерживает образ не более 512 КиБ")
|
||||
self.image = image
|
||||
self.target = target
|
||||
self.offset = 0
|
||||
self.block_size = self.DEFAULT_BLOCK_SIZE
|
||||
self.stage = "ping"
|
||||
self.finished = False
|
||||
self._sequence = 0
|
||||
self._expected_sequence = 0
|
||||
self._expected_type = 0
|
||||
self._response: SetFrame | None = None
|
||||
self._reassembler = CanReassembler()
|
||||
|
||||
def _request(self, message_type: int, payload: bytes = b"") -> list[tr.Frame]:
|
||||
self._sequence = (self._sequence + 1) & 0xFFFF
|
||||
if self._sequence == 0:
|
||||
self._sequence = 1
|
||||
self._expected_sequence = self._sequence
|
||||
self._expected_type = int(message_type)
|
||||
packet = build_frame(
|
||||
SetFrame(
|
||||
message_type=message_type,
|
||||
sequence=self._sequence,
|
||||
payload=payload,
|
||||
flags=FrameFlag.ACK_REQUIRED | FrameFlag.PRIORITY,
|
||||
source=self.HOST_NODE,
|
||||
destination=self.target.node_id,
|
||||
)
|
||||
)
|
||||
address = CanAddress(
|
||||
destination=self.target.node_id,
|
||||
source=self.HOST_NODE,
|
||||
priority=1,
|
||||
channel=self.CHANNEL,
|
||||
)
|
||||
return [tr.build_frame(item.can_id, item.data, to_can=True)
|
||||
for item in segment(packet, address)]
|
||||
|
||||
def start(self) -> list[tr.Frame]:
|
||||
return self._request(MessageType.PING)
|
||||
|
||||
def abort(self) -> list[tr.Frame]:
|
||||
return self._request(MessageType.FW_ABORT)
|
||||
|
||||
def accepts(self, frame: tr.Frame) -> bool:
|
||||
if frame.to_can or not frame.ide:
|
||||
return False
|
||||
try:
|
||||
address = CanAddress.unpack(frame.can_id)
|
||||
except SetProtocolError:
|
||||
return False
|
||||
if address.source != self.target.node_id or address.destination != self.HOST_NODE:
|
||||
return False
|
||||
try:
|
||||
packet = self._reassembler.feed(
|
||||
CanFrame(frame.can_id, frame.data), int(perf_counter() * 1000)
|
||||
)
|
||||
if packet is None:
|
||||
return False
|
||||
response = decode_datagram(packet)
|
||||
except SetProtocolError:
|
||||
return False
|
||||
if (
|
||||
not response.flags & FrameFlag.RESPONSE
|
||||
or response.source != self.target.node_id
|
||||
or response.destination != self.HOST_NODE
|
||||
or response.sequence != self._expected_sequence
|
||||
or response.message_type != self._expected_type
|
||||
):
|
||||
return False
|
||||
self._response = response
|
||||
return True
|
||||
|
||||
def _begin(self) -> list[tr.Frame]:
|
||||
begin = FirmwareBegin(
|
||||
image_size=len(self.image.data),
|
||||
image_crc32=self.image.crc32,
|
||||
image_version=self.image.version,
|
||||
base_address=self.image.base_address,
|
||||
slot=0,
|
||||
block_size=self.block_size,
|
||||
sha256=bytes.fromhex(self.image.sha256),
|
||||
flags=FirmwareFlag.RESUME | FirmwareFlag.ERASE_SLOT,
|
||||
)
|
||||
self.stage = "begin"
|
||||
return self._request(MessageType.FW_BEGIN, begin.encode())
|
||||
|
||||
def _next_data_or_end(self) -> tuple[list[tr.Frame], str]:
|
||||
if self.offset >= len(self.image.data):
|
||||
self.stage = "end"
|
||||
payload = struct.pack(
|
||||
"<II32s", len(self.image.data), self.image.crc32,
|
||||
bytes.fromhex(self.image.sha256),
|
||||
)
|
||||
return self._request(MessageType.FW_END, payload), "Проверка CRC32 и SHA-256"
|
||||
self.stage = "data"
|
||||
data = self.image.data[self.offset : self.offset + self.block_size]
|
||||
return (
|
||||
self._request(MessageType.FW_DATA, encode_firmware_data(self.offset, data)),
|
||||
"Передача блоков SETProtocol v2",
|
||||
)
|
||||
|
||||
def handle_status(self, _frame: tr.Frame) -> tuple[list[tr.Frame], str]:
|
||||
response, self._response = self._response, None
|
||||
if response is None:
|
||||
return [], ""
|
||||
status, body = decode_response(response)
|
||||
if status != Status.OK:
|
||||
try:
|
||||
name = Status(status).name
|
||||
except ValueError:
|
||||
name = "0x%04X" % status
|
||||
raise RuntimeError(name)
|
||||
|
||||
if self.stage == "ping":
|
||||
if len(body) != 4:
|
||||
raise RuntimeError("PING: неверная длина ответа")
|
||||
self.stage = "device_info"
|
||||
return self._request(MessageType.DEVICE_INFO), "Чтение информации об устройстве"
|
||||
|
||||
if self.stage == "device_info":
|
||||
info = DeviceInfo.decode(body)
|
||||
if self.target.device_class and info.device_class != self.target.device_class:
|
||||
raise RuntimeError(
|
||||
"device class 0x%04X вместо 0x%04X"
|
||||
% (info.device_class, self.target.device_class)
|
||||
)
|
||||
if not self.target.hardware_min <= info.hardware_version <= self.target.hardware_max:
|
||||
raise RuntimeError("hardware version устройства вне разрешённого диапазона")
|
||||
self.stage = "capabilities"
|
||||
return self._request(MessageType.CAPABILITIES), "Проверка возможностей устройства"
|
||||
|
||||
if self.stage == "capabilities":
|
||||
capabilities = Capabilities.decode(body)
|
||||
if not capabilities.features & Feature.FIRMWARE:
|
||||
raise RuntimeError("устройство не объявило поддержку firmware update")
|
||||
self.block_size = min(
|
||||
self.DEFAULT_BLOCK_SIZE,
|
||||
capabilities.max_payload - 12,
|
||||
)
|
||||
if self.block_size <= 0:
|
||||
raise RuntimeError("устройство объявило слишком маленький CAN firmware MTU")
|
||||
return self._begin(), "Начало SETProtocol v2 firmware session"
|
||||
|
||||
if self.stage == "begin":
|
||||
if len(body) != 4:
|
||||
raise RuntimeError("FW_BEGIN: неверный next_offset")
|
||||
self.offset = int.from_bytes(body, "little")
|
||||
if self.offset > len(self.image.data):
|
||||
raise RuntimeError("FW_BEGIN: next_offset за пределами образа")
|
||||
return self._next_data_or_end()
|
||||
|
||||
if self.stage == "data":
|
||||
if len(body) != 4:
|
||||
raise RuntimeError("FW_DATA: неверный next_offset")
|
||||
next_offset = int.from_bytes(body, "little")
|
||||
expected = min(self.offset + self.block_size, len(self.image.data))
|
||||
if next_offset != expected:
|
||||
raise RuntimeError(
|
||||
"FW_DATA: подтверждён offset %d вместо %d" % (next_offset, expected)
|
||||
)
|
||||
self.offset = next_offset
|
||||
return self._next_data_or_end()
|
||||
|
||||
if self.stage == "end":
|
||||
if len(body) != 4 or int.from_bytes(body, "little") != len(self.image.data):
|
||||
raise RuntimeError("FW_END: устройство не подтвердило полный образ")
|
||||
self.stage = "activate"
|
||||
return self._request(MessageType.FW_ACTIVATE), "Активация образа"
|
||||
|
||||
if self.stage == "activate":
|
||||
self.finished = True
|
||||
return [], "Прошивка по CAN (SETProtocol v2) завершена"
|
||||
return [], ""
|
||||
|
||||
@property
|
||||
def percent(self) -> int:
|
||||
return min(100, int(self.offset * 100 / len(self.image.data)))
|
||||
47
python/protocan/stm32_boot.py
Normal file
47
python/protocan/stm32_boot.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Packets and constants for the STM32 system-memory UART bootloader (AN3155)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
ACK = 0x79
|
||||
NACK = 0x1F
|
||||
SYNC = b"\x7f"
|
||||
|
||||
|
||||
def command(code: int) -> bytes:
|
||||
"""Encode an AN3155 command and its one's-complement checksum."""
|
||||
if not 0 <= code <= 0xFF:
|
||||
raise ValueError("Код команды вне диапазона байта")
|
||||
return bytes((code, code ^ 0xFF))
|
||||
|
||||
|
||||
def address(value: int) -> bytes:
|
||||
"""Encode a big-endian 32-bit address followed by XOR checksum."""
|
||||
if not 0 <= value <= 0xFFFFFFFF:
|
||||
raise ValueError("Адрес вне диапазона u32")
|
||||
raw = value.to_bytes(4, "big")
|
||||
return raw + bytes((raw[0] ^ raw[1] ^ raw[2] ^ raw[3],))
|
||||
|
||||
|
||||
def write_payload(data: bytes) -> bytes:
|
||||
"""Encode one Write Memory payload (1..256 bytes)."""
|
||||
if not 1 <= len(data) <= 256:
|
||||
raise ValueError("Блок STM32 должен содержать от 1 до 256 байт")
|
||||
count = len(data) - 1
|
||||
checksum = count
|
||||
for value in data:
|
||||
checksum ^= value
|
||||
return bytes((count,)) + data + bytes((checksum,))
|
||||
|
||||
|
||||
MASS_ERASE = b"\xff\x00"
|
||||
|
||||
|
||||
def erase_pages_payload(pages: list[int]) -> bytes:
|
||||
"""Encode the standard Erase Memory page list used by STM32F1."""
|
||||
if not pages or len(pages) > 256 or any(not 0 <= page <= 0xFF for page in pages):
|
||||
raise ValueError("Список страниц STM32 должен содержать 1..256 номеров")
|
||||
body = bytes((len(pages) - 1, *pages))
|
||||
checksum = 0
|
||||
for value in body:
|
||||
checksum ^= value
|
||||
return body + bytes((checksum,))
|
||||
118
python/protocan/tms_firmware.py
Normal file
118
python/protocan/tms_firmware.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""BALZAM/PM67 firmware protocol, ported from Gui_Android Tms2812Protocol.
|
||||
|
||||
Addresses count 16-bit words; transfer lengths count bytes.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
|
||||
from protocan.tms2812 import crc16_modbus, build_upload_request, decode_upload_reply
|
||||
|
||||
CMD_LOAD = 51
|
||||
CMD_UPLOAD = 52
|
||||
CMD_TFLASH = 55
|
||||
CMD_INITLOAD = 58
|
||||
CMD_EXTEND = 60
|
||||
|
||||
|
||||
def packet(controller: int, command: int, payload: bytes) -> bytes:
|
||||
raw = bytes((controller, command)) + payload
|
||||
return raw + struct.pack("<H", crc16_modbus(raw))
|
||||
|
||||
|
||||
def init_load(controller: int, ram: int, size: int) -> bytes:
|
||||
return packet(controller, CMD_INITLOAD, struct.pack("<II", ram, size))
|
||||
|
||||
|
||||
def load_data(controller: int, data: bytes) -> bytes:
|
||||
return packet(controller, CMD_LOAD, data)
|
||||
|
||||
|
||||
def tflash(controller: int, ram: int, flash: int, size: int) -> bytes:
|
||||
return packet(controller, CMD_TFLASH, struct.pack("<III", ram, flash, size))
|
||||
|
||||
|
||||
def extend(controller: int, first: int, second: int, size: int, code: int, board: int) -> bytes:
|
||||
return packet(controller, CMD_EXTEND, struct.pack("<IIIBB", first, second, size, code, board))
|
||||
|
||||
|
||||
def extend_result(raw: bytes) -> tuple[int, int, int]:
|
||||
return struct.unpack_from("<III", raw, 2)
|
||||
|
||||
|
||||
def normalize_reply(raw: bytes, controller: int, command: int, size: int) -> bytes | None:
|
||||
"""Accept omitted reserved tail bytes only after the receive settle interval."""
|
||||
tail = 4 if command == CMD_UPLOAD and size > 6 else 2
|
||||
if not size - tail <= len(raw) <= size or raw[:2] != bytes((controller, command)):
|
||||
return None
|
||||
full = raw.ljust(size, b"\0")
|
||||
if tail == 4:
|
||||
try:
|
||||
decode_upload_reply(full, controller, size - 8)
|
||||
except ValueError:
|
||||
return None
|
||||
elif crc16_modbus(full[:-4]) != int.from_bytes(full[-4:-2], "little"):
|
||||
return None
|
||||
return full
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TmsTarget:
|
||||
controller: int = 10
|
||||
ram: int = 0x000A0000
|
||||
flash: int = 0x00110000
|
||||
block_size: int = 256
|
||||
kind: str = "tms"
|
||||
board: int = 0
|
||||
load_only: bool = False
|
||||
|
||||
def validate(self, size: int) -> None:
|
||||
if not 0 <= self.controller <= 255:
|
||||
raise ValueError("Адрес контроллера должен быть 0…255")
|
||||
if not 256 <= self.block_size <= 65534 or self.block_size % 2:
|
||||
raise ValueError("Блок LOAD должен быть чётным, 256…65534 байт")
|
||||
if size <= 0:
|
||||
raise ValueError("Образ пуст")
|
||||
words = (size + 1) // 2
|
||||
if not 0 <= self.ram <= 0xFFFFFFFF or self.ram + words > 0x100000000:
|
||||
raise ValueError("Диапазон RAM выходит за пределы uint32")
|
||||
if self.kind not in ("tms", "spartan2e", "spartan6"):
|
||||
raise ValueError("Неизвестный протокол прошивки TMS")
|
||||
if not 0 <= self.board <= 255 or not 0 <= self.flash <= 0xFFFFFFFF:
|
||||
raise ValueError("Неверный адрес платы или памяти")
|
||||
if not self.load_only and self.kind == "tms" and (not 0x00100000 <= self.flash < 0x00180000 or self.flash + words > 0x00180000):
|
||||
raise ValueError("Flash должна находиться в диапазоне слов 0x00100000…0x00180000")
|
||||
|
||||
|
||||
def programming_steps(data: bytes, target: TmsTarget):
|
||||
"""Yield requests; send each validated response back into the generator."""
|
||||
t = target
|
||||
t.validate(len(data))
|
||||
for offset in range(0, len(data), t.block_size):
|
||||
block = data[offset:offset + t.block_size]
|
||||
ram = t.ram + offset // 2
|
||||
yield init_load(t.controller, ram, len(block)), 6, 1500, "CMD_INITLOAD", int(offset * 85 / len(data))
|
||||
yield load_data(t.controller, block), 6, 3000, "CMD_LOAD", int(offset * 85 / len(data))
|
||||
if t.load_only:
|
||||
return
|
||||
if t.kind != "tms":
|
||||
code = 6 if t.kind == "spartan2e" else 10
|
||||
response = yield extend(t.controller, t.ram, t.flash, len(data), code, t.board), 18, 600000, "CMD_EXTEND: запись платы", 85
|
||||
words, error, repeats = extend_result(response)
|
||||
if error or words != (len(data) + 1) // 2:
|
||||
raise ValueError(f"Ошибка записи платы: код {error}, обработано {words} слов, повторов {repeats}")
|
||||
if t.kind == "spartan2e":
|
||||
response = yield extend(t.controller, t.ram, t.flash, len(data), 17, t.board), 18, 600000, "CMD_EXTEND: проверка EEPROM", 95
|
||||
words, error, repeats = extend_result(response)
|
||||
if error or words != (len(data) + 1) // 2:
|
||||
raise ValueError(f"Проверка EEPROM не пройдена: код {error}, обработано {words} слов")
|
||||
return
|
||||
yield tflash(t.controller, t.ram, t.flash, len(data)), 6, 180000, "CMD_TFLASH: запись Flash", 85
|
||||
for offset in range(0, len(data), 256):
|
||||
block = data[offset:offset + 256]
|
||||
count = (len(block) + 1) & ~1
|
||||
response = yield build_upload_request(t.controller, t.flash + offset // 2, count), count + 8, 30000, "CMD_UPLOAD: проверка Flash", 90 + int(offset * 10 / len(data))
|
||||
actual = decode_upload_reply(response, t.controller, count)[:len(block)]
|
||||
if actual != block:
|
||||
raise ValueError(f"Проверка Flash не пройдена: блок по адресу слова 0x{t.flash + offset // 2:08X}")
|
||||
17
python/pyproject.toml
Normal file
17
python/pyproject.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "setcorp-templates"
|
||||
version = "0.1.0"
|
||||
description = "Reusable SET device protocols, transports and logic analyzers"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
[project.optional-dependencies]
|
||||
qt = ["PySide6>=6.7,<7; python_version >= '3.9'", "PySide2>=5.15,<6; python_version < '3.9'"]
|
||||
saleae = ["logic2-automation>=1.0.7,<2"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["altera_logic*", "logic_analyzer*", "set_devices*", "protocan*", "setprotocol*"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user