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`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||||
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
|
| [`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 в
|
Это C-библиотека описания работающего образа: SemVer, дата/время компиляции,
|
||||||
include path, запуск генератора до компиляции и декодирование build ID как двух
|
8 символов build ID и сериализация контракта v1. Она не обращается к сети,
|
||||||
ASCII-байтов в каждом логическом слове.
|
не читает версию из сервера и не записывает 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)`, сохраняя старый
|
KONOR публикует эти 24 байта ответом `FIRMWARE_INFO (0x03)`, сохраняя старый
|
||||||
32-байтовый `DEVICE_INFO` без изменений.
|
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 protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
|
||||||
set(SETPROTOCOL_V2_SOURCES
|
set(SETPROTOCOL_V2_SOURCES
|
||||||
|
src/altera_logic.c
|
||||||
|
src/altera_stream.c
|
||||||
src/set_protocol.c
|
src/set_protocol.c
|
||||||
src/set_can.c
|
src/set_can.c
|
||||||
src/set_firmware.c
|
src/set_firmware.c
|
||||||
@@ -79,6 +81,12 @@ endif()
|
|||||||
option(SETP_BUILD_TESTS "Build host tests" ON)
|
option(SETP_BUILD_TESTS "Build host tests" ON)
|
||||||
if(SETP_BUILD_TESTS)
|
if(SETP_BUILD_TESTS)
|
||||||
enable_testing()
|
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)
|
add_executable(test_plot tests/test_plot.c)
|
||||||
target_link_libraries(test_plot PRIVATE setprotocol_static)
|
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] = []
|
JNI_INCLUDES: list[Path] = []
|
||||||
SOURCES = [
|
SOURCES = [
|
||||||
ROOT / "src" / name for name in (
|
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",
|
"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",
|
"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",
|
"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` из корня
|
Для пересборки HTML запустите `doc/setcan/build-html.bat` из корня
|
||||||
репозитория `templates`.
|
репозитория `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."""
|
"""Переносимые модули и тонкая 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 (
|
from .native import (
|
||||||
NativeCore, NativeCoreUnavailable, NativeFrame, NativeGuiFrame,
|
NativeCore, NativeCoreUnavailable, NativeFrame, NativeGuiFrame,
|
||||||
NativeGuiParser, NativeParser, NativeProtocol, NativeProtocolUnavailable,
|
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 = {
|
DEVICE_TYPE_NAMES = {
|
||||||
0x0: "Верхний уровень",
|
0x0: "Верхний уровень",
|
||||||
|
0x6: "Логические анализаторы",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEVICE_ADDRESS_NAMES = {
|
DEVICE_ADDRESS_NAMES = {
|
||||||
|
(0x6, 0xE): "Altera Logic",
|
||||||
(0x7, 0xD): "configurator",
|
(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