Compare commits
9 Commits
feature/sh
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| de5dc18a43 | |||
| d44d57a7aa | |||
| b972b6f33c | |||
| 454baeed98 | |||
| 01ffc0e496 | |||
| 3c4ac9963d | |||
| d9eb7dd9ad | |||
| f6787163dc | |||
| 5f8fc07f2a |
@@ -17,6 +17,7 @@ set(SETPROTOCOL_V2_SOURCES
|
|||||||
|
|
||||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||||
set(SETPROTOCOL_LEGACY_SOURCES
|
set(SETPROTOCOL_LEGACY_SOURCES
|
||||||
|
src/balsam_can.c
|
||||||
src/gui_catalog.c
|
src/gui_catalog.c
|
||||||
src/gui_frame.c
|
src/gui_frame.c
|
||||||
src/pcan_abi.c
|
src/pcan_abi.c
|
||||||
@@ -94,6 +95,9 @@ if(SETP_BUILD_TESTS)
|
|||||||
add_executable(test_abi tests/test_abi.c)
|
add_executable(test_abi tests/test_abi.c)
|
||||||
target_link_libraries(test_abi PRIVATE setprotocol_static)
|
target_link_libraries(test_abi PRIVATE setprotocol_static)
|
||||||
add_test(NAME stable_abi COMMAND test_abi)
|
add_test(NAME stable_abi COMMAND test_abi)
|
||||||
|
add_executable(test_balsam_can tests/test_balsam_can.c)
|
||||||
|
target_link_libraries(test_balsam_can PRIVATE setprotocol_static)
|
||||||
|
add_test(NAME legacy_balsam_can COMMAND test_balsam_can)
|
||||||
add_executable(test_trends tests/test_trends.c)
|
add_executable(test_trends tests/test_trends.c)
|
||||||
target_link_libraries(test_trends PRIVATE setprotocol_static)
|
target_link_libraries(test_trends PRIVATE setprotocol_static)
|
||||||
add_test(NAME shared_trends COMMAND test_trends)
|
add_test(NAME shared_trends COMMAND test_trends)
|
||||||
|
|||||||
61
c/set-protocol/include/balsam_can.h
Normal file
61
c/set-protocol/include/balsam_can.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @file balsam_can.h
|
||||||
|
* @brief Legacy Balsam 167 extended-CAN register frames.
|
||||||
|
*
|
||||||
|
* The wire layout is taken from Balsam_167_periph/Source/Internal/ecan.c:
|
||||||
|
* one big-endian address/mask word followed by three big-endian register words.
|
||||||
|
*/
|
||||||
|
#ifndef BALSAM_CAN_H
|
||||||
|
#define BALSAM_CAN_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define BALSAM_CAN_BASE_ID 0x00BA0000UL
|
||||||
|
#define BALSAM_CAN_NODE_COUNT 13U
|
||||||
|
#define BALSAM_CAN_DATA_OFFSET 0x10U
|
||||||
|
#define BALSAM_CAN_DLC 8U
|
||||||
|
#define BALSAM_CAN_REGISTER_COUNT 3U
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
BALSAM_CAN_TO_NODE = 0,
|
||||||
|
BALSAM_CAN_FROM_NODE = 1
|
||||||
|
} balsam_can_direction_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t device;
|
||||||
|
uint8_t direction;
|
||||||
|
uint8_t present_mask;
|
||||||
|
uint16_t start_address;
|
||||||
|
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
|
||||||
|
} balsam_can_frame_t;
|
||||||
|
|
||||||
|
/** Return non-zero for the command/data/terminal IDs used by Balsam 167. */
|
||||||
|
int balsam_can_is_id(uint32_t can_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode an 8-byte Balsam frame. Returns 1 on success, 0 for another CAN ID,
|
||||||
|
* -1 for invalid arguments and -2 for a Balsam ID with a non-8-byte payload.
|
||||||
|
*/
|
||||||
|
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||||
|
balsam_can_frame_t *output);
|
||||||
|
|
||||||
|
/** Human-readable Russian name of a Balsam device. */
|
||||||
|
const char *balsam_can_device_name(uint8_t device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the register name from the Balsam 167 data table into output.
|
||||||
|
* Returns the required length (excluding NUL); an empty string means unknown.
|
||||||
|
*/
|
||||||
|
size_t balsam_can_register_name(uint8_t device, uint16_t address,
|
||||||
|
char *output, size_t output_size);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* BALSAM_CAN_H */
|
||||||
@@ -45,6 +45,14 @@ typedef struct {
|
|||||||
uint8_t payload[PCAN_ABI_GUI_PAYLOAD_MAX];
|
uint8_t payload[PCAN_ABI_GUI_PAYLOAD_MAX];
|
||||||
} pcan_abi_gui_frame_t;
|
} pcan_abi_gui_frame_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t device;
|
||||||
|
uint8_t direction;
|
||||||
|
uint8_t present_mask;
|
||||||
|
uint16_t start_address;
|
||||||
|
uint16_t values[3];
|
||||||
|
} pcan_abi_balsam_frame_t;
|
||||||
|
|
||||||
PCAN_ABI_API uint32_t pcan_abi_version(void);
|
PCAN_ABI_API uint32_t pcan_abi_version(void);
|
||||||
|
|
||||||
PCAN_ABI_API uint32_t pcan_abi_id_pack(uint8_t priority, uint8_t route,
|
PCAN_ABI_API uint32_t pcan_abi_id_pack(uint8_t priority, uint8_t route,
|
||||||
@@ -58,6 +66,13 @@ PCAN_ABI_API void pcan_abi_id_unpack(uint32_t raw, uint8_t *priority,
|
|||||||
|
|
||||||
PCAN_ABI_API uint16_t pcan_abi_crc16(const uint8_t *data, size_t size);
|
PCAN_ABI_API uint16_t pcan_abi_crc16(const uint8_t *data, size_t size);
|
||||||
|
|
||||||
|
PCAN_ABI_API int pcan_abi_balsam_decode(uint32_t can_id,
|
||||||
|
const uint8_t *data, size_t size,
|
||||||
|
pcan_abi_balsam_frame_t *output);
|
||||||
|
PCAN_ABI_API const char *pcan_abi_balsam_device_name(uint8_t device);
|
||||||
|
PCAN_ABI_API size_t pcan_abi_balsam_register_name(
|
||||||
|
uint8_t device, uint16_t address, char *output, size_t output_size);
|
||||||
|
|
||||||
PCAN_ABI_API size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
|
PCAN_ABI_API size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
|
||||||
uint32_t can_id,
|
uint32_t can_id,
|
||||||
const uint8_t *data, uint8_t dlc,
|
const uint8_t *data, uint8_t dlc,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ enum set_plot_operation {
|
|||||||
SET_PLOT_VALUE = 3, /* fraction,low,high,inverted -> value (not clamped) */
|
SET_PLOT_VALUE = 3, /* fraction,low,high,inverted -> value (not clamped) */
|
||||||
SET_PLOT_DRAG = 4, /* initial,deltaPixels,length,low,high,inverted -> clamped value */
|
SET_PLOT_DRAG = 4, /* initial,deltaPixels,length,low,high,inverted -> clamped value */
|
||||||
SET_PLOT_TICK_STEP = 5, /* range,lengthPixels -> nice step */
|
SET_PLOT_TICK_STEP = 5, /* range,lengthPixels -> nice step */
|
||||||
SET_PLOT_DELTA = 6 /* A,B,multiplier -> (B-A)*multiplier */
|
SET_PLOT_DELTA = 6, /* A,B,multiplier -> (B-A)*multiplier */
|
||||||
|
SET_PLOT_DB_DELTA = 7 /* A,B -> 20*log10(abs(B/A)); zero is invalid */
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Version of this plot ABI, independently of the transport ABI. */
|
/** Version of this plot ABI, independently of the transport ABI. */
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#ifndef SETPROTOCOL_H
|
#ifndef SETPROTOCOL_H
|
||||||
#define SETPROTOCOL_H
|
#define SETPROTOCOL_H
|
||||||
|
|
||||||
|
#include "balsam_can.h"
|
||||||
|
|
||||||
/* Основной SET protocol v2. */
|
/* Основной SET protocol v2. */
|
||||||
#include "set_protocol.h"
|
#include "set_protocol.h"
|
||||||
#include "set_can.h"
|
#include "set_can.h"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ LOCAL_SRC_FILES := \
|
|||||||
set_plot_jni.c \
|
set_plot_jni.c \
|
||||||
../../src/set_trends.c \
|
../../src/set_trends.c \
|
||||||
../../src/set_spectrum.c \
|
../../src/set_spectrum.c \
|
||||||
|
../../src/balsam_can.c \
|
||||||
../../src/gui_catalog.c \
|
../../src/gui_catalog.c \
|
||||||
../../src/gui_frame.c \
|
../../src/gui_frame.c \
|
||||||
../../src/pcan_abi.c \
|
../../src/pcan_abi.c \
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ workspace, up to 2×16384 doubles). `trends/SpectrumAnalyzer` maps timestamps an
|
|||||||
errors but does not duplicate FFT/filter math. Run it off the UI thread.
|
errors but does not duplicate FFT/filter math. Run it off the UI thread.
|
||||||
`trends/PlotViewport` is a toolkit-free normalized zoom/pan model.
|
`trends/PlotViewport` is a toolkit-free normalized zoom/pan model.
|
||||||
|
|
||||||
|
`legacycan/LegacyCanTerminal.kt` contains the two historical CAN_terminal wire
|
||||||
|
formats, the complete Projects.ini node/command catalog, and shared codecs for
|
||||||
|
register writes and command frames. Android UI code must use this module instead
|
||||||
|
of reproducing the Delphi byte rotation or CAN-ID routing rules.
|
||||||
|
|
||||||
`kotlin/ru/setcorp/setprotocol/update/FirmwareCatalog.kt` is a UI-independent
|
`kotlin/ru/setcorp/setprotocol/update/FirmwareCatalog.kt` is a UI-independent
|
||||||
firmware release client. It reads the optional `firmware.releases` array from
|
firmware release client. It reads the optional `firmware.releases` array from
|
||||||
the shared `update.json`, accepts only HTTPS assets, limits their size and
|
the shared `update.json`, accepts only HTTPS assets, limits their size and
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ object NativeSetProtocol {
|
|||||||
): Long
|
): Long
|
||||||
external fun nativeUnpackId(raw: Long): IntArray?
|
external fun nativeUnpackId(raw: Long): IntArray?
|
||||||
external fun nativeCrc16(input: ByteArray): Int
|
external fun nativeCrc16(input: ByteArray): Int
|
||||||
|
external fun nativeBalsamDecode(canId: Long, input: ByteArray): IntArray?
|
||||||
|
external fun nativeBalsamDeviceName(device: Int): String
|
||||||
|
external fun nativeBalsamRegisterName(device: Int, address: Int): String
|
||||||
external fun nativeEncodeFrame(
|
external fun nativeEncodeFrame(
|
||||||
sequence: Int,
|
sequence: Int,
|
||||||
flags: Int,
|
flags: Int,
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package ru.setcorp.setprotocol.balsam
|
||||||
|
|
||||||
|
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||||
|
|
||||||
|
data class BalsamRegister(val address: Int, val value: Int, val name: String) {
|
||||||
|
val displayName: String get() = name.ifBlank { "R%04X".format(address) }
|
||||||
|
val signedValue: Int get() = if (value < 0x8000) value else value - 0x10000
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BalsamFrame(
|
||||||
|
val canId: Long,
|
||||||
|
val device: Int,
|
||||||
|
val deviceName: String,
|
||||||
|
val fromDevice: Boolean,
|
||||||
|
val startAddress: Int,
|
||||||
|
val presentMask: Int,
|
||||||
|
val registers: List<BalsamRegister>,
|
||||||
|
) {
|
||||||
|
fun summary(): String {
|
||||||
|
val direction = if (fromDevice) "данные" else "команда"
|
||||||
|
val values = registers.joinToString { "${it.displayName}=0x%04X (%d)".format(it.value, it.signedValue) }
|
||||||
|
.ifEmpty { "нет отмеченных регистров" }
|
||||||
|
return "BALZAM · $deviceName · $direction · $values"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared parser for Balsam_167_periph eCAN frames. */
|
||||||
|
object BalsamCanProtocol {
|
||||||
|
const val BASE_ID = 0x00BA_0000L
|
||||||
|
const val TERMINAL_REQUEST_ID = 0x00BA_001CL
|
||||||
|
const val TERMINAL_RESPONSE_ID = 0x00BA_000CL
|
||||||
|
const val PULT_REQUEST_ID = 0x0074_5019L
|
||||||
|
const val PULT_RESPONSE_ID = 0x0074_5009L
|
||||||
|
|
||||||
|
fun isLegacyId(canId: Long): Boolean {
|
||||||
|
val relative = (canId and 0x1FFF_FFFFL) - BASE_ID
|
||||||
|
return relative in 0L..12L || relative in 0x10L..0x1CL ||
|
||||||
|
canId == PULT_REQUEST_ID || canId == PULT_RESPONSE_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decode(canId: Long, data: ByteArray): BalsamFrame? {
|
||||||
|
if (!isRegisterId(canId) || data.size != 8) return null
|
||||||
|
val native = if (NativeSetProtocol.available) {
|
||||||
|
NativeSetProtocol.nativeBalsamDecode(canId, data)
|
||||||
|
} else null
|
||||||
|
val words = native ?: fallbackDecode(canId, data)
|
||||||
|
val device = words[0]
|
||||||
|
val mask = words[2]
|
||||||
|
val start = words[3]
|
||||||
|
val registers = (0..2).filter { mask and (4 shr it) != 0 }.map { index ->
|
||||||
|
val address = start + index
|
||||||
|
BalsamRegister(address, words[4 + index], registerName(device, address))
|
||||||
|
}
|
||||||
|
return BalsamFrame(
|
||||||
|
canId and 0x1FFF_FFFFL,
|
||||||
|
device,
|
||||||
|
deviceName(device),
|
||||||
|
words[1] == 1,
|
||||||
|
start,
|
||||||
|
mask,
|
||||||
|
registers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun summary(canId: Long, data: ByteArray? = null): String =
|
||||||
|
data?.let { decode(canId, it)?.summary() } ?: when (canId) {
|
||||||
|
TERMINAL_REQUEST_ID -> "BALZAM legacy · запрос терминала"
|
||||||
|
TERMINAL_RESPONSE_ID -> "BALZAM legacy · ответ терминалу"
|
||||||
|
PULT_REQUEST_ID -> "BALZAM legacy · данные пульта"
|
||||||
|
PULT_RESPONSE_ID -> "BALZAM legacy · команда пульту"
|
||||||
|
in (BASE_ID + 0x10L)..(BASE_ID + 0x1BL) ->
|
||||||
|
"BALZAM legacy · данные · ${deviceName((canId - BASE_ID - 0x0FL).toInt())}"
|
||||||
|
in BASE_ID..(BASE_ID + 0x0BL) ->
|
||||||
|
"BALZAM legacy · команда · ${deviceName((canId - BASE_ID + 1L).toInt())}"
|
||||||
|
else -> "BALZAM legacy · неизвестный ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isRegisterId(canId: Long): Boolean {
|
||||||
|
val relative = (canId and 0x1FFF_FFFFL) - BASE_ID
|
||||||
|
return relative in 0L..12L || relative in 0x10L..0x1CL
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fallbackDecode(canId: Long, data: ByteArray): IntArray {
|
||||||
|
val relative = (canId and 0x1FFF_FFFFL) - BASE_ID
|
||||||
|
val header = u16be(data, 0)
|
||||||
|
return intArrayOf(
|
||||||
|
((relative and 0x0F) + 1).toInt(),
|
||||||
|
if (relative >= 0x10) 1 else 0,
|
||||||
|
(header ushr 13) and 7,
|
||||||
|
header and 0x1FFF,
|
||||||
|
u16be(data, 2), u16be(data, 4), u16be(data, 6),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun u16be(data: ByteArray, offset: Int): Int =
|
||||||
|
((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF)
|
||||||
|
|
||||||
|
private fun deviceName(device: Int): String = if (NativeSetProtocol.available) {
|
||||||
|
NativeSetProtocol.nativeBalsamDeviceName(device)
|
||||||
|
} else listOf(
|
||||||
|
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1", "Силовой блок 2",
|
||||||
|
"УМП 1", "УМП 2", "Двигатель", "ВЭП", "Задатчик", "Узел 10", "Узел 11",
|
||||||
|
"Узел 12", "Терминал",
|
||||||
|
).getOrElse(device - 1) { "Неизвестный узел" }
|
||||||
|
|
||||||
|
private fun registerName(device: Int, address: Int): String =
|
||||||
|
if (NativeSetProtocol.available) NativeSetProtocol.nativeBalsamRegisterName(device, address)
|
||||||
|
else when {
|
||||||
|
device in 1..2 && address in 0x18..0x2B -> "Показания T° ${address - 0x17}"
|
||||||
|
device in 3..4 && address in 0x18..0x27 -> "Показания T° ${address - 0x17}"
|
||||||
|
device == 7 && address in 0x18..0x1F -> "Показания T° ${address - 0x17}"
|
||||||
|
address == 0x17 -> "Состояние джамперов"
|
||||||
|
address == 0x7F -> "Команды"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package ru.setcorp.setprotocol.legacycan
|
||||||
|
|
||||||
|
/** Wire formats implemented by the historical CAN_terminal application. */
|
||||||
|
enum class LegacyCanFormat {
|
||||||
|
/** Address and a three-bit presence mask are carried in DATA[4..5]. */
|
||||||
|
ROTATING_THREE_WORDS,
|
||||||
|
|
||||||
|
/** Register address is carried in CAN ID[6:0], followed by up to four words. */
|
||||||
|
ADDRESS_IN_IDENTIFIER,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class LegacyCanSource { TO_DEVICE, FROM_DEVICE }
|
||||||
|
|
||||||
|
data class LegacyCanPacket(
|
||||||
|
val address: Int,
|
||||||
|
val mask: Int,
|
||||||
|
val values: List<Int>,
|
||||||
|
val source: LegacyCanSource,
|
||||||
|
) {
|
||||||
|
val presentValues: List<Pair<Int, Int>>
|
||||||
|
get() = values.mapIndexedNotNull { index, value ->
|
||||||
|
if (formatUses(index)) address + index to value else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatUses(index: Int): Boolean = mask == 0xFF || mask and (4 shr index) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LegacyCanWireFrame(val canId: Long, val data: ByteArray)
|
||||||
|
|
||||||
|
data class LegacyCanRegisterValue(
|
||||||
|
val address: Int,
|
||||||
|
val value: Int = 0,
|
||||||
|
val source: LegacyCanSource? = null,
|
||||||
|
val revision: Long = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LegacyCanNode(
|
||||||
|
val index: Int,
|
||||||
|
val rsAddress: Int,
|
||||||
|
val canAddress: Int,
|
||||||
|
val rxId: Long,
|
||||||
|
val txId: Long,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LegacyCanProject(
|
||||||
|
val name: String,
|
||||||
|
val format: LegacyCanFormat,
|
||||||
|
val baseId: Long,
|
||||||
|
val idOffset: Long,
|
||||||
|
val nodes: List<LegacyCanNode>,
|
||||||
|
val commandNames: List<String>,
|
||||||
|
) {
|
||||||
|
fun nodeFor(canId: Long): LegacyCanNode? {
|
||||||
|
val normalized = LegacyCanTerminalProtocol.routingId(format, canId)
|
||||||
|
return nodes.firstOrNull { it.rxId == normalized || it.txId == normalized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared, UI-independent codec for the two protocols found in CAN_terminal.pas.
|
||||||
|
* Values are unsigned 16-bit words; callers can interpret them as signed with
|
||||||
|
* [signedWord].
|
||||||
|
*/
|
||||||
|
object LegacyCanTerminalProtocol {
|
||||||
|
fun emptyRegisterBank(): List<LegacyCanRegisterValue> =
|
||||||
|
List(128) { LegacyCanRegisterValue(it) }
|
||||||
|
|
||||||
|
fun applyPacket(
|
||||||
|
bank: List<LegacyCanRegisterValue>,
|
||||||
|
packet: LegacyCanPacket,
|
||||||
|
revision: Long,
|
||||||
|
): List<LegacyCanRegisterValue> {
|
||||||
|
require(bank.size == 128) { "Банк должен содержать 128 регистров" }
|
||||||
|
val updates = packet.presentValues.toMap()
|
||||||
|
return bank.map { current ->
|
||||||
|
updates[current.address]?.let { value ->
|
||||||
|
current.copy(value = value, source = packet.source, revision = revision)
|
||||||
|
} ?: current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun routingId(format: LegacyCanFormat, canId: Long): Long = when (format) {
|
||||||
|
LegacyCanFormat.ROTATING_THREE_WORDS -> canId and 0x1FFF_FFFFL
|
||||||
|
LegacyCanFormat.ADDRESS_IN_IDENTIFIER -> canId and 0x1FF0_0000L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decode(
|
||||||
|
format: LegacyCanFormat,
|
||||||
|
node: LegacyCanNode,
|
||||||
|
canId: Long,
|
||||||
|
data: ByteArray,
|
||||||
|
): LegacyCanPacket? {
|
||||||
|
val route = routingId(format, canId)
|
||||||
|
val source = when (route) {
|
||||||
|
node.txId -> LegacyCanSource.TO_DEVICE
|
||||||
|
node.rxId -> LegacyCanSource.FROM_DEVICE
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
return when (format) {
|
||||||
|
LegacyCanFormat.ROTATING_THREE_WORDS -> {
|
||||||
|
if (data.size != 8) return null
|
||||||
|
val mask = (u8(data[4]) ushr 5) and 7
|
||||||
|
val address = ((u8(data[4]) and 0x1F) shl 8) or u8(data[5])
|
||||||
|
LegacyCanPacket(address, mask, listOf(u16be(data, 6), u16be(data, 0), u16be(data, 2)), source)
|
||||||
|
}
|
||||||
|
LegacyCanFormat.ADDRESS_IN_IDENTIFIER -> {
|
||||||
|
if (data.isEmpty() || data.size > 8 || data.size % 2 != 0) return null
|
||||||
|
LegacyCanPacket(
|
||||||
|
address = (canId and 0x7F).toInt(),
|
||||||
|
mask = 0xFF,
|
||||||
|
values = data.indices.step(2).map { u16be(data, it) },
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeWrite(
|
||||||
|
format: LegacyCanFormat,
|
||||||
|
canId: Long,
|
||||||
|
address: Int,
|
||||||
|
values: List<Int>,
|
||||||
|
): LegacyCanWireFrame {
|
||||||
|
require(address in 0..127) { "Адрес регистра должен быть в диапазоне 0..127" }
|
||||||
|
val maximum = if (format == LegacyCanFormat.ADDRESS_IN_IDENTIFIER) 4 else 3
|
||||||
|
require(values.size in 1..maximum) { "Нужно от 1 до $maximum слов данных" }
|
||||||
|
values.forEach { require(it in 0..0xFFFF) { "Значение должно быть в диапазоне 0..65535" } }
|
||||||
|
return when (format) {
|
||||||
|
LegacyCanFormat.ADDRESS_IN_IDENTIFIER -> LegacyCanWireFrame(
|
||||||
|
(canId and 0x1FF0_0000L) + address,
|
||||||
|
values.flatMap { listOf((it ushr 8).toByte(), it.toByte()) }.toByteArray(),
|
||||||
|
)
|
||||||
|
LegacyCanFormat.ROTATING_THREE_WORDS -> {
|
||||||
|
val padded = values + List(3 - values.size) { 0 }
|
||||||
|
val mask = when (values.size) { 1 -> 4; 2 -> 6; else -> 7 }
|
||||||
|
val data = byteArrayOf(
|
||||||
|
(padded[1] ushr 8).toByte(), padded[1].toByte(),
|
||||||
|
(padded[2] ushr 8).toByte(), padded[2].toByte(),
|
||||||
|
((mask shl 5) or (address ushr 8)).toByte(), address.toByte(),
|
||||||
|
(padded[0] ushr 8).toByte(), padded[0].toByte(),
|
||||||
|
)
|
||||||
|
LegacyCanWireFrame(canId and 0x1FFF_FFFFL, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeCommand(project: LegacyCanProject, node: LegacyCanNode, commandIndex: Int): LegacyCanWireFrame {
|
||||||
|
require(commandIndex in 0..16) { "Номер команды должен быть в диапазоне 0..16" }
|
||||||
|
val value = if (commandIndex < 16) 1 shl commandIndex else 0
|
||||||
|
return encodeWrite(project.format, node.rxId, 127, listOf(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun signedWord(value: Int): Int = if (value < 0x8000) value else value - 0x10000
|
||||||
|
|
||||||
|
private fun u8(value: Byte): Int = value.toInt() and 0xFF
|
||||||
|
private fun u16be(data: ByteArray, offset: Int): Int = (u8(data[offset]) shl 8) or u8(data[offset + 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Project table migrated from CAN_terminal/Projects.ini (Windows-1251). */
|
||||||
|
object LegacyCanProjects {
|
||||||
|
private val defaultCommands = listOf(
|
||||||
|
"Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw",
|
||||||
|
"-", "-", "-", "-", "-", "-", "Reset", "Nothing at all",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun project(
|
||||||
|
name: String,
|
||||||
|
baseId: Long = 0,
|
||||||
|
offset: Long = 0x10,
|
||||||
|
format: LegacyCanFormat = LegacyCanFormat.ROTATING_THREE_WORDS,
|
||||||
|
specs: List<List<Any>>,
|
||||||
|
commands: List<String> = defaultCommands,
|
||||||
|
): LegacyCanProject {
|
||||||
|
val shift = if (format == LegacyCanFormat.ADDRESS_IN_IDENTIFIER) 20 else 0
|
||||||
|
val actualOffset = if (format == LegacyCanFormat.ADDRESS_IN_IDENTIFIER) 1L shl 28 else offset
|
||||||
|
val nodes = specs.map { spec ->
|
||||||
|
val index = spec[0] as Int
|
||||||
|
val canAddress = spec[1] as Int
|
||||||
|
val rsAddress = spec[2] as Int
|
||||||
|
val nodeName = spec[3] as String
|
||||||
|
val routed = canAddress.toLong() shl shift
|
||||||
|
LegacyCanNode(index, rsAddress, canAddress, baseId + routed, baseId + actualOffset + routed, nodeName)
|
||||||
|
}
|
||||||
|
return LegacyCanProject(name, format, baseId, actualOffset, nodes, commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun s(index: Int, can: Int, rs: Int, name: String): List<Any> = listOf(index, can, rs, name)
|
||||||
|
|
||||||
|
val all: List<LegacyCanProject> = listOf(
|
||||||
|
project("Буксир", 0x0031_8200, specs = listOf(
|
||||||
|
s(0, 0, 1, "УКСС СБ"), s(1, 1, 2, "БКСС ГД"), s(2, 2, 3, "УКСВЭП"), s(3, 3, 4, "Задатчик"),
|
||||||
|
)),
|
||||||
|
project("СЭДБМ", 0x0105_1020, specs = listOf(
|
||||||
|
s(0,0,0,"УКСС СК1 СБ1"),s(1,1,1,"УКСС СК2 СБ1"),s(2,2,2,"УКСС СК3 СБ1"),s(3,3,3,"УКСС СК4 СБ1"),
|
||||||
|
s(4,4,4,"УКССВЭП СБ1"),s(5,5,5,"Задатчик СБ1"),s(6,6,6,"БТР ИТЭС"),s(8,0x20,8,"УКСС СК1 СБ2"),
|
||||||
|
s(9,0x21,9,"УКСС СК2 СБ2"),s(10,0x22,10,"УКСС СК3 СБ2"),s(11,0x23,11,"УКСС СК4 СБ2"),
|
||||||
|
s(12,0x24,12,"УКССВЭП СБ2"),s(13,0x25,13,"Задатчик СБ2"),s(14,0x26,14,"УКСС БОИН"),s(15,0x27,15,"УКСВЭП БОИН"),
|
||||||
|
), commands = defaultCommands.toMutableList().also { it[7]="Raw"; it[8]="HiVolt" }),
|
||||||
|
project("Ледокол", 0x001C_E020, -0x20, specs = listOf(
|
||||||
|
s(0,0,1,"УКСС БВ1 ПЧ1"),s(8,1,2,"УКСС БВ1 ПЧ2"),s(1,2,3,"УКСС БВ1 ПЧ1"),s(9,3,4,"УКСС БВ2 ПЧ2"),
|
||||||
|
s(2,4,5,"УКСС БИ1 ПЧ1"),s(10,5,6,"УКСС БИ1 ПЧ2"),s(3,6,7,"УКСС БИ2 ПЧ1"),s(11,7,8,"УКСС БИ2 ПЧ2"),
|
||||||
|
s(4,8,9,"УКССВЭП1 ПЧ1"),s(12,9,10,"УКССВЭП1 ПЧ2"),s(5,10,11,"УКССВЭП2 ПЧ1"),s(13,11,12,"УКССВЭП2 ПЧ2"),
|
||||||
|
), commands = defaultCommands.toMutableList().also { it[4]="Raw"; it[5]="Read"; it[6]="ExtLamp"; it[7]="ExtLite"; it[8]="No log" }),
|
||||||
|
project("Бальзам", 0x00BA_0000, specs = listOf(
|
||||||
|
s(0,0,1,"БКСС Тр1"),s(8,1,2,"БКСС Тр2"),s(1,2,3,"УКСС СБ1"),s(9,3,4,"УКСС СБ2"),
|
||||||
|
s(2,4,5,"УКСС УМП1"),s(10,5,6,"УКСС УМП2"),s(3,6,7,"БКСС ГД"),s(4,7,9,"Задатчик"),s(5,8,11,"УКСС ВЭП"),
|
||||||
|
), commands = defaultCommands.toMutableList().also { it[6]="Stop";it[7]="Start";it[8]="Init";it[9]="Tune";it[10]="Secret";it[11]="Light";it[12]="Raw" }),
|
||||||
|
project("23550", 0x0023_5500, specs = listOf(
|
||||||
|
s(0,0,1,"Задатчик"),s(1,1,2,"Выносной пульт"),s(2,2,3,"УКСВЭП"),s(3,3,4,"БКСС ГД"),
|
||||||
|
), commands = defaultCommands.toMutableList().also { it[5]="Read";it[7]="Send";it[8]="-" }),
|
||||||
|
project("23550.X", format = LegacyCanFormat.ADDRESS_IN_IDENTIFIER, specs = listOf(
|
||||||
|
s(0,0,1,"Задатчик"),s(1,1,2,"Выносной пульт"),s(2,2,3,"УКСВЭП"),s(3,3,4,"БКСС ГД"),
|
||||||
|
), commands = defaultCommands.toMutableList().also { it[5]="Read";it[7]="Send";it[8]="-" }),
|
||||||
|
project("23550.2", format = LegacyCanFormat.ADDRESS_IN_IDENTIFIER, specs = listOf(
|
||||||
|
s(0,0,1,"Задатчик"),s(1,1,2,"Выносной пульт"),s(2,2,3,"БКСС ГД"),s(3,4,4,"УКСС СИ СБ1"),
|
||||||
|
s(4,6,6,"УКСС СВФ СБ1"),s(5,8,8,"УКСВЭП СБ1"),s(11,5,5,"УКСС СИ СБ2"),s(12,7,7,"УКСС СВФ СБ2"),
|
||||||
|
s(13,9,9,"УКСВЭП СБ2"),s(16,0x1F,16,"BroadCast"),
|
||||||
|
), commands = defaultCommands.toMutableList().also { it[5]="Calc";it[7]="Send" }),
|
||||||
|
project("Янтарь", 0x0021_3000, specs = listOf(
|
||||||
|
s(0,0,1,"УКСС БВ"),s(1,1,2,"УКСС БИ1"),s(2,2,3,"УКСС БИ2"),s(3,3,4,"БКСС ГД"),
|
||||||
|
s(4,4,5,"УКСВЭП"),s(5,5,6,"Задатчик"),s(6,6,7,"Выносной пульт"),
|
||||||
|
)),
|
||||||
|
project(
|
||||||
|
"23550 БСУ", 0x0CEB_0F1, -0x10,
|
||||||
|
specs = listOf(s(0,0,0,"БСУ1"),s(1,1,1,"БСУ2")),
|
||||||
|
commands = List(16) { "-" } + "Nothing at all",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,3 +30,5 @@ data class PlotBounds(val left: Double, val right: Double, val bottom: Double, v
|
|||||||
|
|
||||||
fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, range, pixels)[0]
|
fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, range, pixels)[0]
|
||||||
fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0]
|
fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0]
|
||||||
|
fun plotDbDelta(a: Double, b: Double): Double? =
|
||||||
|
runCatching { NativePlot.call(7, a, b)[0] }.getOrNull()
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package ru.setcorp.setprotocol.trends
|
package ru.setcorp.setprotocol.trends
|
||||||
|
|
||||||
/** Normalized top-left viewport; independent of pixels, units, toolkit and samples. */
|
/** Normalized top-left viewport; independent of pixels, units, toolkit and samples. */
|
||||||
data class PlotViewport(val x: Double = 0.0, val y: Double = 0.0, val width: Double = 1.0, val height: Double = 1.0) {
|
data class PlotViewport(val x: Double = 0.0, val y: Double = 0.0, val width: Double = 1.0, val height: Double = 1.0,
|
||||||
|
val locked: Boolean = false) {
|
||||||
fun transform(zoomX: Double = 1.0, zoomY: Double = 1.0, panX: Double = 0.0, panY: Double = 0.0,
|
fun transform(zoomX: Double = 1.0, zoomY: Double = 1.0, panX: Double = 0.0, panY: Double = 0.0,
|
||||||
focusX: Double = 0.5, focusY: Double = 0.5): PlotViewport {
|
focusX: Double = 0.5, focusY: Double = 0.5): PlotViewport {
|
||||||
|
if (locked) return this
|
||||||
val result = NativePlot.call(0, x, y, width, height, zoomX, zoomY, panX, panY, focusX, focusY)
|
val result = NativePlot.call(0, x, y, width, height, zoomX, zoomY, panX, panY, focusX, focusY)
|
||||||
return PlotViewport(result[0], result[1], result[2], result[3])
|
return PlotViewport(result[0], result[1], result[2], result[3], locked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ enum class TrendMarker(val title: String, val horizontal: Boolean) {
|
|||||||
data class TrendMarkers(
|
data class TrendMarkers(
|
||||||
val xEnabled: Boolean = true,
|
val xEnabled: Boolean = true,
|
||||||
val yEnabled: Boolean = false,
|
val yEnabled: Boolean = false,
|
||||||
|
val xPairs: Int = 1,
|
||||||
|
val yPairs: Int = 1,
|
||||||
val selected: TrendMarker = TrendMarker.A,
|
val selected: TrendMarker = TrendMarker.A,
|
||||||
val a: Double? = null,
|
val a: Double? = null,
|
||||||
val b: Double? = null,
|
val b: Double? = null,
|
||||||
@@ -20,6 +22,9 @@ data class TrendMarkers(
|
|||||||
val g: Double? = null,
|
val g: Double? = null,
|
||||||
val h: Double? = null,
|
val h: Double? = null,
|
||||||
) {
|
) {
|
||||||
|
init {
|
||||||
|
require(xPairs in 1..2 && yPairs in 1..2) { "Количество пар маркеров должно быть от 1 до 2" }
|
||||||
|
}
|
||||||
fun value(marker: TrendMarker): Double? = when (marker) {
|
fun value(marker: TrendMarker): Double? = when (marker) {
|
||||||
TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d
|
TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d
|
||||||
TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h
|
TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h
|
||||||
@@ -49,5 +54,10 @@ data class TrendMarkers(
|
|||||||
}.sortedWith(compareBy<Pair<TrendMarker, Double>> { it.second }
|
}.sortedWith(compareBy<Pair<TrendMarker, Double>> { it.second }
|
||||||
.thenBy { if (it.first == selected) 0 else 1 }).firstOrNull()?.first
|
.thenBy { if (it.first == selected) 0 else 1 }).firstOrNull()?.first
|
||||||
}
|
}
|
||||||
fun enabled(marker: TrendMarker): Boolean = if (marker.horizontal) yEnabled else xEnabled
|
fun enabled(marker: TrendMarker): Boolean = when (marker) {
|
||||||
|
TrendMarker.A, TrendMarker.B -> xEnabled
|
||||||
|
TrendMarker.C, TrendMarker.D -> xEnabled && xPairs >= 2
|
||||||
|
TrendMarker.E, TrendMarker.F -> yEnabled
|
||||||
|
TrendMarker.G, TrendMarker.H -> yEnabled && yPairs >= 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class FirmwareCatalogClient(private val userAgent: String) {
|
|||||||
require(fileName.matches(Regex("[A-Za-zА-Яа-яЁё0-9._ -]+\\.(bin|hex)", RegexOption.IGNORE_CASE))) {
|
require(fileName.matches(Regex("[A-Za-zА-Яа-яЁё0-9._ -]+\\.(bin|hex)", RegexOption.IGNORE_CASE))) {
|
||||||
"Некорректное имя файла прошивки №${index + 1}"
|
"Некорректное имя файла прошивки №${index + 1}"
|
||||||
}
|
}
|
||||||
require(transport in setOf("rs485", "can", "stm32")) { "Некорректный канал прошивки №${index + 1}" }
|
require(transport in setOf("rs485", "can", "stm32", "tms")) { "Некорректный канал прошивки №${index + 1}" }
|
||||||
val baseAddress = row.opt("baseAddress")?.takeUnless { it == JSONObject.NULL }?.toString()?.let(::parseAddress)
|
val baseAddress = row.opt("baseAddress")?.takeUnless { it == JSONObject.NULL }?.toString()?.let(::parseAddress)
|
||||||
add(FirmwareRelease(
|
add(FirmwareRelease(
|
||||||
product, versionName, versionCode, imageUrl, fileName, sha256,
|
product, versionName, versionCode, imageUrl, fileName, sha256,
|
||||||
|
|||||||
@@ -6,6 +6,53 @@
|
|||||||
#include "setprotocol_abi.h"
|
#include "setprotocol_abi.h"
|
||||||
#include "set_trends.h"
|
#include "set_trends.h"
|
||||||
#include "set_spectrum.h"
|
#include "set_spectrum.h"
|
||||||
|
#include "balsam_can.h"
|
||||||
|
|
||||||
|
JNIEXPORT jintArray JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeBalsamDecode(
|
||||||
|
JNIEnv *env, jobject self, jlong can_id, jbyteArray input)
|
||||||
|
{
|
||||||
|
(void)self;
|
||||||
|
balsam_can_frame_t frame;
|
||||||
|
jsize size;
|
||||||
|
jbyte data[BALSAM_CAN_DLC];
|
||||||
|
jint values[7];
|
||||||
|
if (input == NULL) return NULL;
|
||||||
|
size = (*env)->GetArrayLength(env, input);
|
||||||
|
if (size != (jsize)BALSAM_CAN_DLC) return NULL;
|
||||||
|
(*env)->GetByteArrayRegion(env, input, 0, size, data);
|
||||||
|
if (balsam_can_decode((uint32_t)can_id, (const uint8_t *)data,
|
||||||
|
(size_t)size, &frame) != 1) return NULL;
|
||||||
|
values[0] = frame.device;
|
||||||
|
values[1] = frame.direction;
|
||||||
|
values[2] = frame.present_mask;
|
||||||
|
values[3] = frame.start_address;
|
||||||
|
values[4] = frame.values[0];
|
||||||
|
values[5] = frame.values[1];
|
||||||
|
values[6] = frame.values[2];
|
||||||
|
jintArray result = (*env)->NewIntArray(env, 7);
|
||||||
|
if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, 7, values);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeBalsamDeviceName(
|
||||||
|
JNIEnv *env, jobject self, jint device)
|
||||||
|
{
|
||||||
|
(void)self;
|
||||||
|
return (*env)->NewStringUTF(env, balsam_can_device_name((uint8_t)device));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeBalsamRegisterName(
|
||||||
|
JNIEnv *env, jobject self, jint device, jint address)
|
||||||
|
{
|
||||||
|
(void)self;
|
||||||
|
char name[128];
|
||||||
|
balsam_can_register_name((uint8_t)device, (uint16_t)address,
|
||||||
|
name, sizeof name);
|
||||||
|
return (*env)->NewStringUTF(env, name);
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT jdoubleArray JNICALL
|
JNIEXPORT jdoubleArray JNICALL
|
||||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrum(
|
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrum(
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ru.setcorp.setprotocol.balsam
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class BalsamCanProtocolTest {
|
||||||
|
@Test
|
||||||
|
fun decodesThreeNamedSensorRegisters() {
|
||||||
|
val frame = requireNotNull(BalsamCanProtocol.decode(
|
||||||
|
0x00BA_0010L,
|
||||||
|
byteArrayOf(0xE0.toByte(), 0x18, 0x00, 0x29, 0xFF.toByte(), 0xFE.toByte(), 0x12, 0x34),
|
||||||
|
))
|
||||||
|
assertEquals(1, frame.device)
|
||||||
|
assertEquals(0x18, frame.startAddress)
|
||||||
|
assertEquals(listOf(41, 0xFFFE, 0x1234), frame.registers.map { it.value })
|
||||||
|
assertEquals("Показания T° 1", frame.registers.first().displayName)
|
||||||
|
assertTrue(frame.summary().contains("Трансформатор 1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ru.setcorp.setprotocol.legacycan
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LegacyCanTerminalTest {
|
||||||
|
@Test fun rotatingFormatRoundTripsAllWordCounts() {
|
||||||
|
val project = LegacyCanProjects.all.first { it.name == "Бальзам" }
|
||||||
|
val node = project.nodes.first()
|
||||||
|
for (values in listOf(listOf(0x1234), listOf(0x1234, 0xABCD), listOf(0x1234, 0xABCD, 0x8001))) {
|
||||||
|
val wire = LegacyCanTerminalProtocol.encodeWrite(project.format, node.rxId, 0x18, values)
|
||||||
|
val decoded = LegacyCanTerminalProtocol.decode(project.format, node, wire.canId, wire.data)!!
|
||||||
|
assertEquals(0x18, decoded.address)
|
||||||
|
assertEquals(values, decoded.presentValues.map { it.second })
|
||||||
|
assertEquals(LegacyCanSource.FROM_DEVICE, decoded.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun addressInIdentifierRoundTripsFourWords() {
|
||||||
|
val project = LegacyCanProjects.all.first { it.name == "23550.2" }
|
||||||
|
val node = project.nodes.first { it.name == "БКСС ГД" }
|
||||||
|
val values = listOf(1, 2, 0x7FFF, 0xFFFF)
|
||||||
|
val wire = LegacyCanTerminalProtocol.encodeWrite(project.format, node.txId, 0x7F, values)
|
||||||
|
val decoded = LegacyCanTerminalProtocol.decode(project.format, node, wire.canId, wire.data)!!
|
||||||
|
assertEquals(values, decoded.values)
|
||||||
|
assertEquals(LegacyCanSource.TO_DEVICE, decoded.source)
|
||||||
|
assertEquals(-1, LegacyCanTerminalProtocol.signedWord(decoded.values.last()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun unrelatedIdDoesNotDecode() {
|
||||||
|
val project = LegacyCanProjects.all.first()
|
||||||
|
assertNull(LegacyCanTerminalProtocol.decode(project.format, project.nodes.first(), 0x123, ByteArray(8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun packetReducerUpdatesOnlyPresentRegisters() {
|
||||||
|
val packet = LegacyCanPacket(10, 5, listOf(11, 22, 33), LegacyCanSource.FROM_DEVICE)
|
||||||
|
val bank = LegacyCanTerminalProtocol.applyPacket(LegacyCanTerminalProtocol.emptyRegisterBank(), packet, 42)
|
||||||
|
assertEquals(listOf(11, 33), listOf(bank[10].value, bank[12].value))
|
||||||
|
assertEquals(0, bank[11].value)
|
||||||
|
assertEquals(42, bank[12].revision)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun catalogContainsAllLegacyProjectsAndNodes() {
|
||||||
|
assertEquals(9, LegacyCanProjects.all.size)
|
||||||
|
assertEquals(10, LegacyCanProjects.all.first { it.name == "23550.2" }.nodes.size)
|
||||||
|
assertEquals("Start", LegacyCanProjects.all.first { it.name == "Бальзам" }.commandNames[7])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,9 @@ class PlotContractTest {
|
|||||||
@Test fun markerCoordinatesSurviveZoomAndDragUsesGestureStart() {
|
@Test fun markerCoordinatesSurviveZoomAndDragUsesGestureStart() {
|
||||||
val full = PlotBounds(1000.0, 2000.0, -10.0, 10.0)
|
val full = PlotBounds(1000.0, 2000.0, -10.0, 10.0)
|
||||||
val markers = TrendMarkers().positioned(full)
|
val markers = TrendMarkers().positioned(full)
|
||||||
|
assertTrue(markers.enabled(TrendMarker.A))
|
||||||
|
assertFalse(markers.enabled(TrendMarker.C))
|
||||||
|
assertTrue(markers.copy(xPairs = 2).enabled(TrendMarker.C))
|
||||||
val zoomed = PlotBounds(1250.0, 1750.0, -5.0, 5.0)
|
val zoomed = PlotBounds(1250.0, 1750.0, -5.0, 5.0)
|
||||||
assertEquals(markers, markers.positioned(zoomed))
|
assertEquals(markers, markers.positioned(zoomed))
|
||||||
val dragged = markers.drag(TrendMarker.A, 50.0, 500.0, zoomed)
|
val dragged = markers.drag(TrendMarker.A, 50.0, 500.0, zoomed)
|
||||||
@@ -34,4 +37,12 @@ class PlotContractTest {
|
|||||||
val crossed = markers.move(TrendMarker.A, 1900.0).move(TrendMarker.B, 1100.0)
|
val crossed = markers.move(TrendMarker.A, 1900.0).move(TrendMarker.B, 1100.0)
|
||||||
assertEquals(-800.0, plotDelta(crossed.a!!, crossed.b!!), 0.0)
|
assertEquals(-800.0, plotDelta(crossed.a!!, crossed.b!!), 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun lockedViewportIgnoresZoomAndPanAndDbUsesAmplitudeRatio() {
|
||||||
|
val locked = PlotViewport(locked = true)
|
||||||
|
assertEquals(locked, locked.transform(zoomX = 2.0, panY = 0.2))
|
||||||
|
assertEquals(20.0, plotDbDelta(1.0, 10.0)!!, 1e-10)
|
||||||
|
assertEquals(-20.0, plotDbDelta(10.0, 1.0)!!, 1e-10)
|
||||||
|
assertNull(plotDbDelta(0.0, 1.0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
c/set-protocol/src/balsam_can.c
Normal file
182
c/set-protocol/src/balsam_can.c
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#include "balsam_can.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static uint16_t get_be16(const uint8_t *data)
|
||||||
|
{
|
||||||
|
return (uint16_t)(((uint16_t)data[0] << 8) | data[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t copy_name(const char *name, char *output, size_t output_size)
|
||||||
|
{
|
||||||
|
size_t length = strlen(name);
|
||||||
|
if ((output != NULL) && (output_size != 0U)) {
|
||||||
|
size_t copied = length < output_size - 1U ? length : output_size - 1U;
|
||||||
|
memcpy(output, name, copied);
|
||||||
|
output[copied] = '\0';
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t indexed_name(const char *prefix, unsigned index,
|
||||||
|
char *output, size_t output_size)
|
||||||
|
{
|
||||||
|
int length;
|
||||||
|
if ((output == NULL) || (output_size == 0U)) {
|
||||||
|
char scratch[64];
|
||||||
|
length = snprintf(scratch, sizeof scratch, "%s %u", prefix, index);
|
||||||
|
} else {
|
||||||
|
length = snprintf(output, output_size, "%s %u", prefix, index);
|
||||||
|
}
|
||||||
|
return length > 0 ? (size_t)length : 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
int balsam_can_is_id(uint32_t can_id)
|
||||||
|
{
|
||||||
|
uint32_t relative = (can_id & 0x1FFFFFFFUL) - BALSAM_CAN_BASE_ID;
|
||||||
|
return relative < BALSAM_CAN_NODE_COUNT
|
||||||
|
|| (relative >= BALSAM_CAN_DATA_OFFSET
|
||||||
|
&& relative < BALSAM_CAN_DATA_OFFSET + BALSAM_CAN_NODE_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||||
|
balsam_can_frame_t *output)
|
||||||
|
{
|
||||||
|
uint32_t relative;
|
||||||
|
uint16_t header;
|
||||||
|
if ((data == NULL) || (output == NULL)) return -1;
|
||||||
|
can_id &= 0x1FFFFFFFUL;
|
||||||
|
if (!balsam_can_is_id(can_id)) return 0;
|
||||||
|
if (size != BALSAM_CAN_DLC) return -2;
|
||||||
|
|
||||||
|
relative = can_id - BALSAM_CAN_BASE_ID;
|
||||||
|
output->direction = relative >= BALSAM_CAN_DATA_OFFSET
|
||||||
|
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
|
||||||
|
output->device = (uint8_t)((relative & 0x0FU) + 1U);
|
||||||
|
header = get_be16(data);
|
||||||
|
output->present_mask = (uint8_t)((header >> 13) & 0x07U);
|
||||||
|
output->start_address = (uint16_t)(header & 0x1FFFU);
|
||||||
|
output->values[0] = get_be16(&data[2]);
|
||||||
|
output->values[1] = get_be16(&data[4]);
|
||||||
|
output->values[2] = get_be16(&data[6]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *balsam_can_device_name(uint8_t device)
|
||||||
|
{
|
||||||
|
static const char *const names[BALSAM_CAN_NODE_COUNT] = {
|
||||||
|
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1",
|
||||||
|
"Силовой блок 2", "УМП 1", "УМП 2", "Двигатель", "ВЭП",
|
||||||
|
"Задатчик", "Узел 10", "Узел 11", "Узел 12", "Терминал"
|
||||||
|
};
|
||||||
|
return (device >= 1U && device <= BALSAM_CAN_NODE_COUNT)
|
||||||
|
? names[device - 1U] : "Неизвестный узел";
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t balsam_can_register_name(uint8_t device, uint16_t address,
|
||||||
|
char *output, size_t output_size)
|
||||||
|
{
|
||||||
|
if ((device == 1U || device == 2U) && address < 20U)
|
||||||
|
return indexed_name("Диагностика T°", address + 1U, output, output_size);
|
||||||
|
if ((device == 1U || device == 2U) && address >= 0x18U && address <= 0x2BU)
|
||||||
|
return indexed_name("Показания T°", address - 0x17U, output, output_size);
|
||||||
|
if ((device == 1U || device == 2U) && address >= 0x30U && address <= 0x43U)
|
||||||
|
return indexed_name("Аварийная уставка T°", address - 0x2FU, output, output_size);
|
||||||
|
if ((device == 1U || device == 2U) && address >= 0x48U && address <= 0x5BU)
|
||||||
|
return indexed_name("Предупредительная уставка T°", address - 0x47U, output, output_size);
|
||||||
|
|
||||||
|
if ((device == 3U || device == 4U) && address < 16U)
|
||||||
|
return indexed_name("Диагностика T°", address + 1U, output, output_size);
|
||||||
|
if ((device == 3U || device == 4U) && address >= 0x18U && address <= 0x27U)
|
||||||
|
return indexed_name("Показания T°", address - 0x17U, output, output_size);
|
||||||
|
if ((device == 3U || device == 4U) && address == 0x28U)
|
||||||
|
return copy_name("Действующее Uвх1", output, output_size);
|
||||||
|
if ((device == 3U || device == 4U) && address == 0x29U)
|
||||||
|
return copy_name("Амплитудное Uвх1", output, output_size);
|
||||||
|
if ((device == 3U || device == 4U) && address == 0x2AU)
|
||||||
|
return copy_name("Действующее Uвх2", output, output_size);
|
||||||
|
if ((device == 3U || device == 4U) && address == 0x2BU)
|
||||||
|
return copy_name("Амплитудное Uвх2", output, output_size);
|
||||||
|
|
||||||
|
if ((device == 5U || device == 6U) && address == 0U)
|
||||||
|
return copy_name("Диагностика Utr a", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 1U)
|
||||||
|
return copy_name("Диагностика Utr c", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 2U)
|
||||||
|
return copy_name("Диагностика Itr a", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 3U)
|
||||||
|
return copy_name("Диагностика Itr c", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 0x18U)
|
||||||
|
return copy_name("Действующее Utr", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 0x19U)
|
||||||
|
return copy_name("Амплитудное Utr", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 0x1AU)
|
||||||
|
return copy_name("Действующее Itr", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 0x1BU)
|
||||||
|
return copy_name("Амплитудное Itr", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 0x1CU)
|
||||||
|
return copy_name("Ток СИФУ, задание (mA*10)", output, output_size);
|
||||||
|
if ((device == 5U || device == 6U) && address == 0x1DU)
|
||||||
|
return copy_name("Ток СИФУ, обратная связь (mA*10)", output, output_size);
|
||||||
|
|
||||||
|
if (device == 7U && address < 8U)
|
||||||
|
return indexed_name("Диагностика T°", address + 1U, output, output_size);
|
||||||
|
if (device == 7U && address >= 0x18U && address <= 0x1FU)
|
||||||
|
return indexed_name("Показания T°", address - 0x17U, output, output_size);
|
||||||
|
|
||||||
|
if (device == 8U) {
|
||||||
|
static const char *const vep_names[] = {
|
||||||
|
"Диагностика 380В Ф1", "Диагностика 380В Ф2",
|
||||||
|
"Диагностика 31В Ф1", "Диагностика 31В Ф2",
|
||||||
|
"Диагностика 31В UC1", "Диагностика 31В UC2",
|
||||||
|
"Диагностика 24В ПУ", "Диагностика 27В ФА",
|
||||||
|
"Диагностика 24В ПК", "Диагностика 15В ДР",
|
||||||
|
"Диагностика +24В ДТ", "Диагностика -24В ДТ",
|
||||||
|
"Диагностика 24В ПМУ", "Диагностика T° 1", "Диагностика T° 2"
|
||||||
|
};
|
||||||
|
static const char *const vep_values[] = {
|
||||||
|
"Показания 380В Ф1", "Показания 380В Ф2", "Показания 31В Ф1",
|
||||||
|
"Показания 31В Ф2", "Показания 31В UC1", "Показания 31В UC2",
|
||||||
|
"Показания 24В ПУ", "Показания 27В ФА", "Показания 24В ПК",
|
||||||
|
"Показания 15В ДР", "Показания +24В ДТ", "Показания -24В ДТ",
|
||||||
|
"Показания 24В ПМУ", "Показания T° 1", "Показания T° 2"
|
||||||
|
};
|
||||||
|
if (address < sizeof vep_names / sizeof vep_names[0])
|
||||||
|
return copy_name(vep_names[address], output, output_size);
|
||||||
|
if (address >= 0x18U && address < 0x18U + sizeof vep_values / sizeof vep_values[0])
|
||||||
|
return copy_name(vep_values[address - 0x18U], output, output_size);
|
||||||
|
if (address == 0x10U)
|
||||||
|
return copy_name("Дискретные датчики (14 бит)", output, output_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device == 9U && address == 0U)
|
||||||
|
return copy_name("Обороты ГЭД, об/мин", output, output_size);
|
||||||
|
if (device == 9U && address == 1U)
|
||||||
|
return copy_name("Обороты ГВ, об/мин", output, output_size);
|
||||||
|
if (device == 9U && address == 2U)
|
||||||
|
return copy_name("Лампы (8 бит)", output, output_size);
|
||||||
|
if (device == 9U && address == 3U)
|
||||||
|
return copy_name("Диоды (16 бит)", output, output_size);
|
||||||
|
if (device == 9U && address == 0x10U)
|
||||||
|
return copy_name("Кнопки (12 бит)", output, output_size);
|
||||||
|
|
||||||
|
if (address == 0x16U)
|
||||||
|
return copy_name("Дискретные входы / кнопки", output, output_size);
|
||||||
|
if (address == 0x17U)
|
||||||
|
return copy_name("Состояние джамперов", output, output_size);
|
||||||
|
if (address == 0x60U)
|
||||||
|
return copy_name("Период быстрого CAN-цикла", output, output_size);
|
||||||
|
if (address == 0x61U)
|
||||||
|
return copy_name("Период медленного CAN-цикла", output, output_size);
|
||||||
|
if (address == 0x62U)
|
||||||
|
return copy_name("Яркость индикации", output, output_size);
|
||||||
|
if (address == 0x63U)
|
||||||
|
return copy_name("Период опроса OWEN", output, output_size);
|
||||||
|
if (address == 0x7EU)
|
||||||
|
return copy_name("Последний режим", output, output_size);
|
||||||
|
if (address == 0x7FU)
|
||||||
|
return copy_name("Команды", output, output_size);
|
||||||
|
|
||||||
|
return copy_name("", output, output_size);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "pcan_crc.h"
|
#include "pcan_crc.h"
|
||||||
|
#include "balsam_can.h"
|
||||||
#include "pcan_frame.h"
|
#include "pcan_frame.h"
|
||||||
#include "pcan_id.h"
|
#include "pcan_id.h"
|
||||||
#include "gui_frame.h"
|
#include "gui_frame.h"
|
||||||
@@ -60,6 +61,33 @@ uint16_t pcan_abi_crc16(const uint8_t *data, size_t size)
|
|||||||
return pcan_crc16(data, size);
|
return pcan_crc16(data, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int pcan_abi_balsam_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||||
|
pcan_abi_balsam_frame_t *output)
|
||||||
|
{
|
||||||
|
balsam_can_frame_t decoded;
|
||||||
|
int status;
|
||||||
|
if (output == NULL) return -1;
|
||||||
|
status = balsam_can_decode(can_id, data, size, &decoded);
|
||||||
|
if (status != 1) return status;
|
||||||
|
output->device = decoded.device;
|
||||||
|
output->direction = decoded.direction;
|
||||||
|
output->present_mask = decoded.present_mask;
|
||||||
|
output->start_address = decoded.start_address;
|
||||||
|
memcpy(output->values, decoded.values, sizeof output->values);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *pcan_abi_balsam_device_name(uint8_t device)
|
||||||
|
{
|
||||||
|
return balsam_can_device_name(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pcan_abi_balsam_register_name(uint8_t device, uint16_t address,
|
||||||
|
char *output, size_t output_size)
|
||||||
|
{
|
||||||
|
return balsam_can_register_name(device, address, output, output_size);
|
||||||
|
}
|
||||||
|
|
||||||
size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
|
size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
|
||||||
uint32_t can_id, const uint8_t *data, uint8_t dlc,
|
uint32_t can_id, const uint8_t *data, uint8_t dlc,
|
||||||
uint8_t *output, size_t output_size)
|
uint8_t *output, size_t output_size)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ static int finite_values(const double *v, size_t n) {
|
|||||||
uint32_t set_plot_abi_version(void) { return 1U; }
|
uint32_t set_plot_abi_version(void) { return 1U; }
|
||||||
|
|
||||||
size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t cap) {
|
size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t cap) {
|
||||||
static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3};
|
static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3, 2};
|
||||||
double span, fraction;
|
double span, fraction;
|
||||||
if (op > SET_PLOT_DELTA || !v || !out || n != sizes[op] ||
|
if (op > SET_PLOT_DB_DELTA || !v || !out || n != sizes[op] ||
|
||||||
cap < (op == SET_PLOT_TRANSFORM ? 4U : 1U)) return 0;
|
cap < (op == SET_PLOT_TRANSFORM ? 4U : 1U)) return 0;
|
||||||
if (op == SET_PLOT_TRANSFORM) {
|
if (op == SET_PLOT_TRANSFORM) {
|
||||||
double w, h, fx, fy;
|
double w, h, fx, fy;
|
||||||
@@ -65,6 +65,10 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SET_PLOT_DELTA: out[0] = (v[1] - v[0]) * v[2]; break;
|
case SET_PLOT_DELTA: out[0] = (v[1] - v[0]) * v[2]; break;
|
||||||
|
case SET_PLOT_DB_DELTA:
|
||||||
|
if (v[0] == 0 || v[1] == 0) return 0;
|
||||||
|
out[0] = 20 * log10(fabs(v[1] / v[0]));
|
||||||
|
break;
|
||||||
default: return 0;
|
default: return 0;
|
||||||
}
|
}
|
||||||
return isfinite(out[0]) ? 1 : 0;
|
return isfinite(out[0]) ? 1 : 0;
|
||||||
|
|||||||
3
c/set-protocol/tests/fixtures/plot-v1.json
vendored
3
c/set-protocol/tests/fixtures/plot-v1.json
vendored
@@ -20,6 +20,9 @@
|
|||||||
{"name":"ticks","op":5,"input":[100,800],"output":[20]},
|
{"name":"ticks","op":5,"input":[100,800],"output":[20]},
|
||||||
{"name":"negative_delta","op":6,"input":[10,0,1],"output":[-10]},
|
{"name":"negative_delta","op":6,"input":[10,0,1],"output":[-10]},
|
||||||
{"name":"milliseconds","op":6,"input":[0,0.01,1000],"output":[10]},
|
{"name":"milliseconds","op":6,"input":[0,0.01,1000],"output":[10]},
|
||||||
|
{"name":"db_gain","op":7,"input":[1,10],"output":[20]},
|
||||||
|
{"name":"db_attenuation","op":7,"input":[10,1],"output":[-20]},
|
||||||
|
{"name":"db_zero_reference","op":7,"input":[0,1],"output":null},
|
||||||
{"name":"zero_range","op":2,"input":[1,1,1,0],"output":null},
|
{"name":"zero_range","op":2,"input":[1,1,1,0],"output":null},
|
||||||
{"name":"zero_pixels","op":4,"input":[0,1,0,0,1,0],"output":null},
|
{"name":"zero_pixels","op":4,"input":[0,1,0,0,1,0],"output":null},
|
||||||
{"name":"bad_viewport","op":0,"input":[0,0,0,1,2,1,0,0,0.5,0.5],"output":null}
|
{"name":"bad_viewport","op":0,"input":[0,0,0,1,2,1,0,0,0.5,0.5],"output":null}
|
||||||
|
|||||||
30
c/set-protocol/tests/test_balsam_can.c
Normal file
30
c/set-protocol/tests/test_balsam_can.c
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "balsam_can.h"
|
||||||
|
|
||||||
|
#define CHECK(x) do { if (!(x)) { \
|
||||||
|
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); return 1; \
|
||||||
|
} } while (0)
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
const uint8_t packet[8] = { 0xE0, 0x18, 0x00, 0x29, 0xFF, 0xFE, 0x12, 0x34 };
|
||||||
|
balsam_can_frame_t frame;
|
||||||
|
char name[64];
|
||||||
|
|
||||||
|
CHECK(balsam_can_is_id(0x00BA0010UL));
|
||||||
|
CHECK(balsam_can_is_id(0x00BA001CUL));
|
||||||
|
CHECK(!balsam_can_is_id(0x00BA001DUL));
|
||||||
|
CHECK(balsam_can_decode(0x00BA0010UL, packet, sizeof packet, &frame) == 1);
|
||||||
|
CHECK(frame.device == 1U);
|
||||||
|
CHECK(frame.direction == BALSAM_CAN_FROM_NODE);
|
||||||
|
CHECK(frame.present_mask == 7U);
|
||||||
|
CHECK(frame.start_address == 0x18U);
|
||||||
|
CHECK(frame.values[0] == 41U && frame.values[1] == 0xFFFEU
|
||||||
|
&& frame.values[2] == 0x1234U);
|
||||||
|
CHECK(balsam_can_register_name(1U, 0x18U, name, sizeof name) > 0U);
|
||||||
|
CHECK(strstr(name, "T° 1") != NULL);
|
||||||
|
CHECK(balsam_can_decode(0x00BA0010UL, packet, 7U, &frame) == -2);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ JNI_INCLUDES: list[Path] = []
|
|||||||
SOURCES = [
|
SOURCES = [
|
||||||
ROOT / "src" / name for name in (
|
ROOT / "src" / name for name in (
|
||||||
"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",
|
||||||
"gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
"balsam_can.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",
|
||||||
"pcan_gas.c",
|
"pcan_gas.c",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ from .native import (
|
|||||||
NativeGuiParser, NativeParser, NativeProtocol, NativeProtocolUnavailable,
|
NativeGuiParser, NativeParser, NativeProtocol, NativeProtocolUnavailable,
|
||||||
get_native_core, get_native_protocol,
|
get_native_core, get_native_protocol,
|
||||||
)
|
)
|
||||||
|
from .balsam import BalsamFrame, BalsamRegister, decode as decode_balsam
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"NativeCore", "NativeCoreUnavailable", "NativeFrame", "NativeGuiFrame",
|
"NativeCore", "NativeCoreUnavailable", "NativeFrame", "NativeGuiFrame",
|
||||||
"NativeGuiParser", "NativeParser", "NativeProtocol",
|
"NativeGuiParser", "NativeParser", "NativeProtocol",
|
||||||
"NativeProtocolUnavailable", "get_native_core", "get_native_protocol",
|
"NativeProtocolUnavailable", "get_native_core", "get_native_protocol",
|
||||||
|
"BalsamFrame", "BalsamRegister", "decode_balsam",
|
||||||
]
|
]
|
||||||
|
|||||||
98
python/protocan/balsam.py
Normal file
98
python/protocan/balsam.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""Balsam 167 legacy CAN register decoder backed by the shared C99 core."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .native import NativeProtocolUnavailable, get_native_protocol
|
||||||
|
|
||||||
|
|
||||||
|
BASE_ID = 0x00BA0000
|
||||||
|
DATA_OFFSET = 0x10
|
||||||
|
NODE_COUNT = 13
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BalsamRegister:
|
||||||
|
address: int
|
||||||
|
value: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signed_value(self) -> int:
|
||||||
|
return self.value if self.value < 0x8000 else self.value - 0x10000
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BalsamFrame:
|
||||||
|
can_id: int
|
||||||
|
device: int
|
||||||
|
device_name: str
|
||||||
|
from_device: bool
|
||||||
|
start_address: int
|
||||||
|
present_mask: int
|
||||||
|
registers: tuple[BalsamRegister, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def summary(self) -> str:
|
||||||
|
direction = "данные" if self.from_device else "команда"
|
||||||
|
values = ", ".join(
|
||||||
|
"%s=0x%04X (%d)" % (item.name or "R%04X" % item.address,
|
||||||
|
item.value, item.signed_value)
|
||||||
|
for item in self.registers
|
||||||
|
) or "нет отмеченных регистров"
|
||||||
|
return "BALZAM · %s · %s · %s" % (self.device_name, direction, values)
|
||||||
|
|
||||||
|
|
||||||
|
def is_balsam_id(can_id: int) -> bool:
|
||||||
|
relative = (can_id & 0x1FFFFFFF) - BASE_ID
|
||||||
|
return (0 <= relative < NODE_COUNT
|
||||||
|
or DATA_OFFSET <= relative < DATA_OFFSET + NODE_COUNT)
|
||||||
|
|
||||||
|
|
||||||
|
def decode(can_id: int, data: bytes, native=None) -> BalsamFrame | None:
|
||||||
|
"""Decode one abstract Balsam frame: BE mask/address plus three BE words."""
|
||||||
|
if native is None:
|
||||||
|
try:
|
||||||
|
native = get_native_protocol()
|
||||||
|
except NativeProtocolUnavailable:
|
||||||
|
return _decode_fallback(can_id, data)
|
||||||
|
status, decoded = native.balsam_decode(can_id, bytes(data))
|
||||||
|
if status == 0:
|
||||||
|
return None
|
||||||
|
if status == -2:
|
||||||
|
raise ValueError("BALZAM CAN frame must contain exactly 8 data bytes")
|
||||||
|
if status != 1 or decoded is None:
|
||||||
|
raise ValueError("invalid BALZAM CAN frame")
|
||||||
|
device, direction, mask, start, values = decoded
|
||||||
|
registers = tuple(
|
||||||
|
BalsamRegister(start + index, values[index],
|
||||||
|
native.balsam_register_name(device, start + index))
|
||||||
|
for index in range(3) if mask & (4 >> index)
|
||||||
|
)
|
||||||
|
return BalsamFrame(can_id & 0x1FFFFFFF, device,
|
||||||
|
native.balsam_device_name(device), direction == 1,
|
||||||
|
start, mask, registers)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_fallback(can_id: int, data: bytes) -> BalsamFrame | None:
|
||||||
|
if not is_balsam_id(can_id):
|
||||||
|
return None
|
||||||
|
if len(data) != 8:
|
||||||
|
raise ValueError("BALZAM CAN frame must contain exactly 8 data bytes")
|
||||||
|
relative = (can_id & 0x1FFFFFFF) - BASE_ID
|
||||||
|
device = (relative & 0x0F) + 1
|
||||||
|
header = int.from_bytes(data[:2], "big")
|
||||||
|
values = tuple(int.from_bytes(data[offset:offset + 2], "big")
|
||||||
|
for offset in (2, 4, 6))
|
||||||
|
names = {
|
||||||
|
1: "Трансформатор 1", 2: "Трансформатор 2",
|
||||||
|
3: "Силовой блок 1", 4: "Силовой блок 2", 5: "УМП 1", 6: "УМП 2",
|
||||||
|
7: "Двигатель", 8: "ВЭП", 9: "Задатчик", 13: "Терминал",
|
||||||
|
}
|
||||||
|
start, mask = header & 0x1FFF, (header >> 13) & 7
|
||||||
|
registers = tuple(BalsamRegister(start + i, values[i], "")
|
||||||
|
for i in range(3) if mask & (4 >> i))
|
||||||
|
return BalsamFrame(can_id & 0x1FFFFFFF, device,
|
||||||
|
names.get(device, "Узел %d" % device),
|
||||||
|
relative >= DATA_OFFSET, start, mask, registers)
|
||||||
226
python/protocan/legacycan.py
Normal file
226
python/protocan/legacycan.py
Normal file
File diff suppressed because one or more lines are too long
@@ -41,6 +41,16 @@ class _AbiGuiFrame(ctypes.Structure):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _AbiBalsamFrame(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("device", ctypes.c_uint8),
|
||||||
|
("direction", ctypes.c_uint8),
|
||||||
|
("present_mask", ctypes.c_uint8),
|
||||||
|
("start_address", ctypes.c_uint16),
|
||||||
|
("values", ctypes.c_uint16 * 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class NativeFrame:
|
class NativeFrame:
|
||||||
sequence: int
|
sequence: int
|
||||||
@@ -118,6 +128,17 @@ class NativeProtocol:
|
|||||||
]
|
]
|
||||||
lib.pcan_abi_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
lib.pcan_abi_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
||||||
lib.pcan_abi_crc16.restype = ctypes.c_uint16
|
lib.pcan_abi_crc16.restype = ctypes.c_uint16
|
||||||
|
lib.pcan_abi_balsam_decode.argtypes = [
|
||||||
|
ctypes.c_uint32, ctypes.c_void_p, ctypes.c_size_t,
|
||||||
|
ctypes.POINTER(_AbiBalsamFrame),
|
||||||
|
]
|
||||||
|
lib.pcan_abi_balsam_decode.restype = ctypes.c_int
|
||||||
|
lib.pcan_abi_balsam_device_name.argtypes = [ctypes.c_uint8]
|
||||||
|
lib.pcan_abi_balsam_device_name.restype = ctypes.c_char_p
|
||||||
|
lib.pcan_abi_balsam_register_name.argtypes = [
|
||||||
|
ctypes.c_uint8, ctypes.c_uint16, ctypes.c_void_p, ctypes.c_size_t,
|
||||||
|
]
|
||||||
|
lib.pcan_abi_balsam_register_name.restype = ctypes.c_size_t
|
||||||
lib.pcan_abi_frame_encode.argtypes = [
|
lib.pcan_abi_frame_encode.argtypes = [
|
||||||
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32,
|
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32,
|
||||||
ctypes.c_void_p, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t,
|
ctypes.c_void_p, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t,
|
||||||
@@ -178,6 +199,27 @@ class NativeProtocol:
|
|||||||
source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
||||||
return int(self.lib.pcan_abi_crc16(source, len(data)))
|
return int(self.lib.pcan_abi_crc16(source, len(data)))
|
||||||
|
|
||||||
|
def balsam_decode(self, can_id: int, data: bytes):
|
||||||
|
source = (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
||||||
|
output = _AbiBalsamFrame()
|
||||||
|
status = int(self.lib.pcan_abi_balsam_decode(
|
||||||
|
can_id, source, len(data), ctypes.byref(output)))
|
||||||
|
if status != 1:
|
||||||
|
return status, None
|
||||||
|
return status, (int(output.device), int(output.direction),
|
||||||
|
int(output.present_mask), int(output.start_address),
|
||||||
|
tuple(int(value) for value in output.values))
|
||||||
|
|
||||||
|
def balsam_device_name(self, device: int) -> str:
|
||||||
|
value = self.lib.pcan_abi_balsam_device_name(device)
|
||||||
|
return value.decode("utf-8") if value else ""
|
||||||
|
|
||||||
|
def balsam_register_name(self, device: int, address: int) -> str:
|
||||||
|
output = ctypes.create_string_buffer(128)
|
||||||
|
self.lib.pcan_abi_balsam_register_name(
|
||||||
|
device, address, output, len(output))
|
||||||
|
return output.value.decode("utf-8")
|
||||||
|
|
||||||
def encode(self, sequence: int, flags: int, can_id: int, data: bytes) -> bytes:
|
def encode(self, sequence: int, flags: int, can_id: int, data: bytes) -> bytes:
|
||||||
if len(data) > 8:
|
if len(data) > 8:
|
||||||
raise ValueError("DLC cannot exceed 8 bytes")
|
raise ValueError("DLC cannot exceed 8 bytes")
|
||||||
|
|||||||
98
python/protocan/periph28335.py
Normal file
98
python/protocan/periph28335.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""Portable RS command helpers from Set_Terminal_28335.
|
||||||
|
|
||||||
|
The byte order and CRC match ``DTrans.pas``/``UNiiefa.pas``. This module is
|
||||||
|
deliberately UI- and serial-port-independent so desktop and Android clients
|
||||||
|
can share the same request builders.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def crc16_modbus(data: bytes, crc: int = 0xFFFF) -> int:
|
||||||
|
for byte in data:
|
||||||
|
crc ^= byte
|
||||||
|
for _ in range(8):
|
||||||
|
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
|
||||||
|
return crc & 0xFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def with_crc(payload: bytes) -> bytes:
|
||||||
|
crc = crc16_modbus(payload)
|
||||||
|
return bytes(payload) + crc.to_bytes(2, "little")
|
||||||
|
|
||||||
|
|
||||||
|
def build_read_registers(controller: int, start: int, count: int) -> bytes:
|
||||||
|
_range("Адрес контроллера", controller, 0xFF)
|
||||||
|
_range("Начальный регистр", start, 0xFFFF)
|
||||||
|
if not 1 <= count <= 128 or start + count > 128:
|
||||||
|
raise ValueError("Диапазон регистров должен находиться в 0..127")
|
||||||
|
return with_crc(bytes((controller, 3)) + start.to_bytes(2, "big")
|
||||||
|
+ count.to_bytes(2, "big"))
|
||||||
|
|
||||||
|
|
||||||
|
def build_write_register(controller: int, address: int, value: int) -> bytes:
|
||||||
|
_range("Адрес контроллера", controller, 0xFF)
|
||||||
|
_range("Адрес регистра", address, 127)
|
||||||
|
_range("Значение", value, 0xFFFF)
|
||||||
|
return with_crc(bytes((controller, 6)) + address.to_bytes(2, "big")
|
||||||
|
+ value.to_bytes(2, "big"))
|
||||||
|
|
||||||
|
|
||||||
|
def build_command(controller: int, command_index: int) -> bytes:
|
||||||
|
if not 0 <= command_index <= 16:
|
||||||
|
raise ValueError("Номер команды должен быть в диапазоне 0..16")
|
||||||
|
value = 1 << command_index if command_index < 16 else 0
|
||||||
|
return build_write_register(controller, 127, value)
|
||||||
|
|
||||||
|
|
||||||
|
def expected_read_response_size(count: int) -> int:
|
||||||
|
return count * 2 + 5
|
||||||
|
|
||||||
|
|
||||||
|
def decode_read_response(data: bytes, count: int) -> tuple[int, ...]:
|
||||||
|
expected = expected_read_response_size(count)
|
||||||
|
if len(data) != expected:
|
||||||
|
raise ValueError(f"Ожидалось {expected} байт, получено {len(data)}")
|
||||||
|
if crc16_modbus(data[:-2]) != int.from_bytes(data[-2:], "little"):
|
||||||
|
raise ValueError("Ошибка CRC ответа")
|
||||||
|
# Historical replies have a three-byte header; registers are big-endian.
|
||||||
|
body = data[3:-2]
|
||||||
|
if len(body) != count * 2:
|
||||||
|
raise ValueError("Неверная длина данных ответа")
|
||||||
|
return tuple(int.from_bytes(body[offset:offset + 2], "big")
|
||||||
|
for offset in range(0, len(body), 2))
|
||||||
|
|
||||||
|
|
||||||
|
def bits_lsb_first(value: int) -> tuple[bool, ...]:
|
||||||
|
_range("Значение", value, 0xFFFF)
|
||||||
|
return tuple(bool(value & (1 << bit)) for bit in range(16))
|
||||||
|
|
||||||
|
|
||||||
|
def word_from_bits(bits) -> int:
|
||||||
|
values = tuple(bool(item) for item in bits)
|
||||||
|
if len(values) != 16:
|
||||||
|
raise ValueError("Должно быть ровно 16 бит")
|
||||||
|
return sum(1 << bit for bit, checked in enumerate(values) if checked)
|
||||||
|
|
||||||
|
|
||||||
|
def signed_word(value: int) -> int:
|
||||||
|
_range("Значение", value, 0xFFFF)
|
||||||
|
return value if value < 0x8000 else value - 0x10000
|
||||||
|
|
||||||
|
|
||||||
|
def _range(name: str, value: int, maximum: int) -> None:
|
||||||
|
if not 0 <= value <= maximum:
|
||||||
|
raise ValueError(f"{name} вне диапазона 0..{maximum}")
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_COMMANDS = {
|
||||||
|
"По умолчанию": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
"23470": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
"23550": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
"23550.2": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all"),
|
||||||
|
"ICE 22220.1-3": ("Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
"ICE 22220.4-5": ("Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
"Бальзам 161": ("Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
"Бальзам 162": ("Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
"Бальзам 163": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||||
|
}
|
||||||
@@ -61,6 +61,12 @@ class PlotMath:
|
|||||||
def delta(self, a: float, b: float, multiplier: float = 1) -> float:
|
def delta(self, a: float, b: float, multiplier: float = 1) -> float:
|
||||||
return self.call(6, a, b, multiplier)[0]
|
return self.call(6, a, b, multiplier)[0]
|
||||||
|
|
||||||
|
def db_delta(self, a: float, b: float) -> Optional[float]:
|
||||||
|
try:
|
||||||
|
return self.call(7, a, b)[0]
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Viewport:
|
class Viewport:
|
||||||
@@ -68,12 +74,15 @@ class Viewport:
|
|||||||
y: float = 0.0
|
y: float = 0.0
|
||||||
width: float = 1.0
|
width: float = 1.0
|
||||||
height: float = 1.0
|
height: float = 1.0
|
||||||
|
locked: bool = False
|
||||||
|
|
||||||
def transform(self, core: PlotMath, zoom_x: float = 1, zoom_y: float = 1,
|
def transform(self, core: PlotMath, zoom_x: float = 1, zoom_y: float = 1,
|
||||||
pan_x: float = 0, pan_y: float = 0,
|
pan_x: float = 0, pan_y: float = 0,
|
||||||
focus_x: float = 0.5, focus_y: float = 0.5) -> "Viewport":
|
focus_x: float = 0.5, focus_y: float = 0.5) -> "Viewport":
|
||||||
|
if self.locked:
|
||||||
|
return self
|
||||||
return Viewport(*core.call(0, self.x, self.y, self.width, self.height,
|
return Viewport(*core.call(0, self.x, self.y, self.width, self.height,
|
||||||
zoom_x, zoom_y, pan_x, pan_y, focus_x, focus_y))
|
zoom_x, zoom_y, pan_x, pan_y, focus_x, focus_y), locked=self.locked)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -102,6 +111,8 @@ class Bounds:
|
|||||||
class Markers:
|
class Markers:
|
||||||
x_enabled: bool = True
|
x_enabled: bool = True
|
||||||
y_enabled: bool = False
|
y_enabled: bool = False
|
||||||
|
x_pairs: int = 1
|
||||||
|
y_pairs: int = 1
|
||||||
selected: Marker = Marker.A
|
selected: Marker = Marker.A
|
||||||
a: Optional[float] = None
|
a: Optional[float] = None
|
||||||
b: Optional[float] = None
|
b: Optional[float] = None
|
||||||
@@ -112,11 +123,21 @@ class Markers:
|
|||||||
g: Optional[float] = None
|
g: Optional[float] = None
|
||||||
h: Optional[float] = None
|
h: Optional[float] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.x_pairs not in (1, 2) or self.y_pairs not in (1, 2):
|
||||||
|
raise ValueError("Marker pair count must be 1 or 2")
|
||||||
|
|
||||||
def value(self, marker: Marker) -> Optional[float]:
|
def value(self, marker: Marker) -> Optional[float]:
|
||||||
return getattr(self, marker.name.lower())
|
return getattr(self, marker.name.lower())
|
||||||
|
|
||||||
def enabled(self, marker: Marker) -> bool:
|
def enabled(self, marker: Marker) -> bool:
|
||||||
return self.y_enabled if marker.horizontal else self.x_enabled
|
if marker in (Marker.A, Marker.B):
|
||||||
|
return self.x_enabled
|
||||||
|
if marker in (Marker.C, Marker.D):
|
||||||
|
return self.x_enabled and self.x_pairs >= 2
|
||||||
|
if marker in (Marker.E, Marker.F):
|
||||||
|
return self.y_enabled
|
||||||
|
return self.y_enabled and self.y_pairs >= 2
|
||||||
|
|
||||||
def move(self, marker: Marker, value: float) -> "Markers":
|
def move(self, marker: Marker, value: float) -> "Markers":
|
||||||
return replace(self, **{marker.name.lower(): value})
|
return replace(self, **{marker.name.lower(): value})
|
||||||
|
|||||||
@@ -392,6 +392,11 @@ class Decoded:
|
|||||||
registers: Optional[List[tuple]] = None
|
registers: Optional[List[tuple]] = None
|
||||||
#: Замечания о нарушениях протокола
|
#: Замечания о нарушениях протокола
|
||||||
warnings: List[str] = field(default_factory=list)
|
warnings: List[str] = field(default_factory=list)
|
||||||
|
#: Имя прикладного протокола для GUI, когда это не ProtoCAN.
|
||||||
|
protocol: str = "ProtoCAN"
|
||||||
|
#: UI labels for protocols whose identifier is not a ProtoCAN bit field.
|
||||||
|
device_label: str = ""
|
||||||
|
message_label: str = ""
|
||||||
|
|
||||||
|
|
||||||
def _ascii(data: bytes) -> str:
|
def _ascii(data: bytes) -> str:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
MAX_MANIFEST_BYTES = 128 * 1024
|
MAX_MANIFEST_BYTES = 128 * 1024
|
||||||
SUPPORTED_TRANSPORTS = frozenset({"rs485", "can", "stm32"})
|
SUPPORTED_TRANSPORTS = frozenset({"rs485", "can", "stm32", "tms"})
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
39
python/tests/test_legacycan.py
Normal file
39
python/tests/test_legacycan.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from protocan.legacycan import (
|
||||||
|
LegacyCanFormat, LegacyCanSource, PROJECTS, apply_packet, decode,
|
||||||
|
empty_register_bank, encode_command, encode_write, signed_word,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalog_matches_legacy_projects():
|
||||||
|
assert [project.name for project in PROJECTS] == [
|
||||||
|
"Буксир", "СЭДБМ", "Ледокол", "Бальзам", "23550", "23550.X",
|
||||||
|
"23550.2", "Янтарь", "23550 БСУ",
|
||||||
|
]
|
||||||
|
assert len(next(project for project in PROJECTS if project.name == "СЭДБМ").nodes) == 15
|
||||||
|
|
||||||
|
|
||||||
|
def test_rotating_three_word_codec_and_bank():
|
||||||
|
project = next(project for project in PROJECTS if project.name == "Бальзам")
|
||||||
|
node = project.nodes[0]
|
||||||
|
wire = encode_write(project.format, node.rx_id, 24, (41, 0xFFFE, 0x1234))
|
||||||
|
assert wire.data == bytes.fromhex("FF FE 12 34 E0 18 00 29")
|
||||||
|
packet = decode(project.format, node, wire.can_id, wire.data)
|
||||||
|
assert packet is not None
|
||||||
|
assert packet.present_values == ((24, 41), (25, 0xFFFE), (26, 0x1234))
|
||||||
|
assert packet.source is LegacyCanSource.FROM_DEVICE
|
||||||
|
bank = apply_packet(empty_register_bank(), packet, 7)
|
||||||
|
assert bank[25].value == 0xFFFE
|
||||||
|
assert bank[25].revision == 7
|
||||||
|
assert signed_word(bank[25].value) == -2
|
||||||
|
|
||||||
|
|
||||||
|
def test_address_in_identifier_codec_and_command():
|
||||||
|
project = next(project for project in PROJECTS if project.name == "23550.2")
|
||||||
|
node = project.nodes[2]
|
||||||
|
assert project.format is LegacyCanFormat.ADDRESS_IN_IDENTIFIER
|
||||||
|
wire = encode_write(project.format, node.tx_id, 17, (1, 2, 0xFFFF, 4))
|
||||||
|
assert wire.can_id & 0x7F == 17
|
||||||
|
assert wire.data == bytes.fromhex("00 01 00 02 FF FF 00 04")
|
||||||
|
command = encode_command(project, node, 7)
|
||||||
|
assert command.can_id & 0x7F == 127
|
||||||
|
assert command.data == b"\x00\x80"
|
||||||
23
python/tests/test_periph28335.py
Normal file
23
python/tests/test_periph28335.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from protocan.periph28335 import (
|
||||||
|
bits_lsb_first, build_command, build_read_registers,
|
||||||
|
build_write_register, crc16_modbus, word_from_bits,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_request_matches_delphi_byte_order():
|
||||||
|
request = build_read_registers(16, 24, 64)
|
||||||
|
assert request[:6] == bytes.fromhex("10 03 00 18 00 40")
|
||||||
|
assert int.from_bytes(request[-2:], "little") == crc16_modbus(request[:-2])
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_and_command_use_register_127():
|
||||||
|
request = build_write_register(16, 7, 0x1234)
|
||||||
|
assert request[:6] == bytes.fromhex("10 06 00 07 12 34")
|
||||||
|
command = build_command(16, 15)
|
||||||
|
assert command[:6] == bytes.fromhex("10 06 00 7F 80 00")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bits_keep_original_lsb_first_order():
|
||||||
|
bits = bits_lsb_first(0x8005)
|
||||||
|
assert bits[0] and bits[2] and bits[15]
|
||||||
|
assert word_from_bits(bits) == 0x8005
|
||||||
@@ -5,6 +5,7 @@ import math
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import unittest
|
import unittest
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
from protocan.plot import Bounds, Marker, Markers, PlotMath, Viewport
|
from protocan.plot import Bounds, Marker, Markers, PlotMath, Viewport
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ class PlotTests(unittest.TestCase):
|
|||||||
def test_markers_stay_in_data_coordinates_and_cross(self):
|
def test_markers_stay_in_data_coordinates_and_cross(self):
|
||||||
bounds = Bounds(1000, 2000, -10, 10)
|
bounds = Bounds(1000, 2000, -10, 10)
|
||||||
markers = Markers().positioned(self.core, bounds)
|
markers = Markers().positioned(self.core, bounds)
|
||||||
|
self.assertTrue(markers.enabled(Marker.A))
|
||||||
|
self.assertFalse(markers.enabled(Marker.C))
|
||||||
|
self.assertTrue(replace(markers, x_pairs=2).enabled(Marker.C))
|
||||||
zoomed = bounds.visible(self.core, Viewport(.25, .25, .5, .5))
|
zoomed = bounds.visible(self.core, Viewport(.25, .25, .5, .5))
|
||||||
self.assertEqual(markers, markers.positioned(self.core, zoomed))
|
self.assertEqual(markers, markers.positioned(self.core, zoomed))
|
||||||
moved = markers.drag(self.core, Marker.A, 50, 500, zoomed)
|
moved = markers.drag(self.core, Marker.A, 50, 500, zoomed)
|
||||||
@@ -46,6 +50,13 @@ class PlotTests(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
self.core.pinch_axis(value, 10, 8)
|
self.core.pinch_axis(value, 10, 8)
|
||||||
|
|
||||||
|
def test_locked_viewport_and_decibel_delta(self):
|
||||||
|
locked = Viewport(locked=True)
|
||||||
|
self.assertEqual(locked, locked.transform(self.core, zoom_x=2, pan_y=.2))
|
||||||
|
self.assertAlmostEqual(20, self.core.db_delta(1, 10))
|
||||||
|
self.assertAlmostEqual(-20, self.core.db_delta(10, 1))
|
||||||
|
self.assertIsNone(self.core.db_delta(0, 1))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user