9 Commits

Author SHA1 Message Date
226c2ca599 chore(merge): объедини pysetterminal с main
Сохрани актуальный генератор firmware identity с экспортом всей папки сборки и временем UTC+3 при объединении веток.
2026-10-09 11:04:32 +03:00
f2956b54cd merge(deif): объедини Android-компоненты с main 2026-10-09 10:55:08 +03:00
eab8dc5e0a fix(firmware-build): support CCS artifact paths and bootstrap identity config 2026-10-08 17:57:04 +03:00
131f306131 fix(firmware-info): используй UTC+3 в именах папок
Переведи только время имени архива в UTC+3, сохрани UTC в метаданных и заголовке. Проверь переход на следующий день и копирование текущей папки.
2026-10-08 17:05:04 +03:00
4fc0ad4e14 feat(firmware-info): копируй папку текущей сборки целиком
Добавь каталог назначения последним аргументом postcompile CCS и Keil. Копируй только архив текущего запуска с сохранением имени папки, всех файлов и проверкой SHA-256. Обнови MD-команды и проверки выбора папки, вложенных файлов и ошибок экспорта.
2026-10-08 16:26:21 +03:00
da055c2787 feat(firmware-info): архивируй сборки по хешу и дате
Создай в postcompile CCS и Keil папку по commit исходников и UTC-времени precompile. Сохрани точные образы, manifest, SHA-256 и метаданные, сохраняя прежние выходные файлы. Опиши копирование каталогов в SETGUI и проверь разные даты, целостность и Git-метаданные.
2026-10-08 15:31:13 +03:00
8fc2e1b5cf fix(firmware-info): подключи Keil к JSON-схеме версии
Добавь prepare/seal для Keil через общий identity.py, образец конфигурации и команды Before/After Build. Для новой схемы не требуется отдельная release-конфигурация CMD или SETGUI. Проверь HEX/BIN, SHA-256, аргументы и пути с пробелами.
2026-10-08 15:13:34 +03:00
b5b2d1ae37 feat(firmware-info): добавь pre/post-build для CCS и Keil
Подключи общий генератор версии и SCI8-конвертацию через templates. Сохрани метаданные и SHA-256 готового BIN, совместимость с Python 3.8 и прежний SETGUI preflight. Опиши настройку CCS и локальное копирование результатов.
2026-10-08 14:58:27 +03:00
7dfa8bd94d Добавить общие компоненты DEIF для Android и интерактивные приборы
Реализовать SAM/DAM, циклическую передачу, heartbeat, привязки и диагностику. Перенести рисунок и управление MASTER/SLAVE из SETGUI, добавить полноэкранный режим и проверки совместимости кадров.
2026-10-04 23:32:49 +03:00
76 changed files with 2388 additions and 3639 deletions

View File

@@ -0,0 +1,111 @@
"""Tests for preserving exact firmware bytes under pre-build provenance folders."""
import json
from pathlib import Path
import sys
import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from identity import archive_build, build_folder_name, export_build, verify
class ArchiveTests(unittest.TestCase):
def metadata(self, stamp="2026-10-08T12:30:00+00:00"):
return {"source": {"commit": "a1b2c3d4" + "0" * 32, "dirty": False},
"templates": {"commit": None, "dirty": None},
"build_id": "NOGIT000", "built_at_utc": stamp,
"version": [1, 2, 3]}
def test_snapshot_preserves_image_and_records_exact_sha(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image = root / "firmware.bin"
payload = bytes(range(256))
image.write_bytes(payload)
metadata = root / "metadata.json"
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "out")
folder = Path(result["directory"])
self.assertEqual(folder.name, "a1b2c3d4_2026-10-08_15-30-00_UTC+3")
self.assertEqual((folder / image.name).read_bytes(), payload)
self.assertEqual(image.read_bytes(), payload)
self.assertEqual(json.loads((folder / "firmware_identity.json").read_text()), self.metadata())
manifest = folder / (image.name + ".manifest.json")
self.assertEqual(verify(folder / image.name, manifest)["size_bytes"], 256)
(folder / image.name).write_bytes(b"corrupted")
with self.assertRaises(ValueError):
verify(folder / image.name, manifest)
def test_prebuild_times_keep_two_builds_separate(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.hex", root / "metadata.json"
image.write_bytes(b":00000001FF\r\n")
folders = []
for stamp in ("2026-10-08T12:30:00+00:00", "2026-10-08T12:31:00+00:00"):
metadata.write_text(json.dumps(self.metadata(stamp)), encoding="utf-8")
folders.append(Path(archive_build(image, metadata, root / "out")["directory"]))
self.assertNotEqual(folders[0], folders[1])
self.assertTrue(all((folder / image.name).is_file() for folder in folders))
def test_export_copies_current_folder_only(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"exact current build")
# A folder with a later timestamp must not be selected for this build.
metadata.write_text(json.dumps(self.metadata("2026-10-08T12:40:00+00:00")), encoding="utf-8")
archive_build(image, metadata, root / "local")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "local", export=root / "exported")
copied = Path(result["exported_directory"])
self.assertEqual([p.name for p in (root / "exported").iterdir()],
["a1b2c3d4_2026-10-08_15-30-00_UTC+3"])
self.assertEqual((copied / image.name).read_bytes(), image.read_bytes())
self.assertTrue((copied / "firmware_identity.json").is_file())
self.assertTrue((copied / (image.name + ".sha256")).is_file())
verify(copied / image.name, copied / (image.name + ".manifest.json"))
def test_complete_folder_export_keeps_nested_files_and_old_destination(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"image")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
folder = Path(archive_build(image, metadata, root / "local")["directory"])
(folder / "notes").mkdir()
(folder / "notes" / "release.txt").write_text("release notes", encoding="utf-8")
old = root / "exported" / "old_build"
old.mkdir(parents=True)
(old / "keep.txt").write_text("keep", encoding="utf-8")
copied = export_build(folder, root / "exported", image.name)
self.assertEqual((copied / "notes" / "release.txt").read_text(), "release notes")
self.assertEqual((old / "keep.txt").read_text(), "keep")
with self.assertRaises(ValueError):
export_build(folder, folder / "recursive", image.name)
def test_offset_normalizes_to_utc_plus_three_and_dirty_is_visible(self):
metadata = self.metadata("2026-10-08T15:30:00+03:00")
metadata["source"]["dirty"] = True
self.assertEqual(build_folder_name(metadata), "a1b2c3d4+_2026-10-08_15-30-00_UTC+3")
def test_folder_date_rolls_over_at_utc_plus_three_midnight(self):
metadata = self.metadata("2026-10-08T22:30:00+00:00")
self.assertEqual(build_folder_name(metadata), "a1b2c3d4_2026-10-09_01-30-00_UTC+3")
self.assertEqual(metadata["built_at_utc"], "2026-10-08T22:30:00+00:00")
def test_archive_without_git_has_explicit_placeholder(self):
metadata = self.metadata()
metadata["source"]["commit"] = None
self.assertEqual(build_folder_name(metadata), "NOGIT000_2026-10-08_15-30-00_UTC+3")
def test_invalid_provenance_cannot_be_a_folder_name(self):
metadata = self.metadata()
metadata["source"]["commit"] = "../outside"
with self.assertRaises(ValueError):
build_folder_name(metadata)
metadata = self.metadata("2026-10-08T12:30:00")
with self.assertRaises(ValueError):
build_folder_name(metadata)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,85 @@
"""Convert a CCS C2000 OUT to SCI8 HEX/BIN and seal its pre-build identity.
Uses only the standard library, TI hex2000 and the shared identity module.
No network, SETGUI, or project-specific paths are required.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import subprocess
import tempfile
from identity import archive_build, seal, verify
def postbuild(compiler: Path, build_dir: Path, artifact: str,
metadata: Path, output: Path, export: Path = None) -> Path:
# CCS may include the configured output directory in this macro.
# Resolve the input separately; output filenames must use only its basename.
artifact_path = Path(artifact)
if not artifact or artifact_path.name in ("", ".", ".."):
raise ValueError("artifact must name an OUT file without its extension")
artifact = artifact_path.name
if any(c in artifact for c in '\\/:*?"<>|'):
raise ValueError("invalid artifact file base name")
source_base = (artifact_path if artifact_path.is_absolute()
else build_dir / artifact_path)
source = Path(str(source_base) + ".out").resolve()
tool = compiler.resolve() / "bin" / "hex2000.exe"
if not tool.is_file():
raise FileNotFoundError(f"C2000 Hex Utility not found: {tool}")
if not source.is_file():
raise FileNotFoundError(f"compiled OUT not found: {source}")
metadata = metadata.resolve()
settings = json.loads(metadata.read_text(encoding="utf-8"))
build_id = settings["build_id"]
if not isinstance(build_id, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id):
raise ValueError("invalid pre-build identity; run precompile before building")
if build_id == "NOGIT000":
print("[firmware-build] Git identity unavailable: using NOGIT000")
output = output.resolve()
output.mkdir(parents=True, exist_ok=True)
# Keep failed conversions and incomplete manifests out of the final folder.
with tempfile.TemporaryDirectory(prefix=".firmware-", dir=output) as temporary:
stage = Path(temporary)
hex_image = stage / (artifact + ".hex")
bin_image = stage / (artifact + "_" + build_id + ".bin")
for image, mode in ((hex_image, "-i"), (bin_image, "-b")):
subprocess.run([str(tool), str(source), "-boot", "-sci8",
"-map", str(image.with_suffix(image.suffix + ".map")),
"-o", str(image), mode], check=True)
if not image.is_file() or not image.stat().st_size:
raise ValueError(f"hex2000 produced no image: {image}")
seal(bin_image, metadata)
verify(bin_image, bin_image.with_suffix(bin_image.suffix + ".manifest.json"))
for item in stage.iterdir():
item.replace(output / item.name)
result = output / bin_image.name
archive_build(result, metadata, output, (
output / hex_image.name,
output / (hex_image.name + ".map"),
output / (bin_image.name + ".map")), export=export)
print(f"[firmware-build] BIN: {result}")
print(f"[firmware-build] Manifest: {result}.manifest.json")
print(f"[firmware-build] SHA-256: {result}.sha256")
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
for name in ("compiler", "build-dir", "metadata", "output"):
parser.add_argument("--" + name, type=Path, required=True)
parser.add_argument("--artifact", required=True)
parser.add_argument("--export", type=Path)
try:
postbuild(**vars(parser.parse_args()))
except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error:
parser.exit(1, f"[firmware-build] ERROR: {error}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -7,10 +7,13 @@ protocol encoding lives here: the generated header contains scalar constants.
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
import hashlib
import json
import os
import re
import shutil
import tempfile
from pathlib import Path
import subprocess
@@ -36,12 +39,14 @@ def git_state(repository: Path) -> dict:
def write_atomic(path: Path, text: str):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(text, encoding="utf-8", newline="\n")
# Built-in open supports newline on Python versions before 3.10.
with open(temporary, "w", encoding="utf-8", newline="\n") as stream:
stream.write(text)
temporary.replace(path)
def prepare(repository: Path, templates: Path, config: Path, output: Path) -> dict:
settings = json.loads(config.read_text(encoding="utf-8"))
settings = json.loads(config.read_text(encoding="utf-8-sig"))
version = settings["version"]
if (len(version) != 3 or any(type(v) is not int for v in version)
or not all(0 <= v <= limit for v, limit in zip(version, (255, 255, 999)))):
@@ -102,6 +107,73 @@ def verify(image: Path, manifest: Path) -> dict:
return metadata
def build_folder_name(metadata: dict) -> str:
"""Use the firmware commit and pre-build time rendered in UTC+3."""
source = metadata.get("source") or {}
dependency = metadata.get("templates") or {}
commit = source.get("commit")
if commit is not None:
if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-fA-F]{40,64}", commit):
raise ValueError("invalid firmware commit in pre-build metadata")
identity = commit[:8].lower()
if source.get("dirty") or dependency.get("dirty"):
identity += "+"
else:
identity = metadata.get("build_id", "NOGIT000")
if not isinstance(identity, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", identity):
raise ValueError("invalid build ID in pre-build metadata")
stamp_text = metadata.get("built_at_utc")
if not isinstance(stamp_text, str):
raise ValueError("missing pre-build UTC timestamp")
stamp = datetime.fromisoformat(stamp_text.replace("Z", "+00:00"))
if stamp.tzinfo is None:
raise ValueError("pre-build timestamp must include a timezone")
stamp = stamp.astimezone(timezone(timedelta(hours=3)))
return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC+3")
def export_build(folder: Path, export: Path, image_name: str) -> Path:
"""Copy the current complete build folder, without scanning old archives."""
folder, export = folder.resolve(), export.resolve()
if export == folder or folder in export.parents:
raise ValueError("export directory must be outside the current build folder")
destination = export / folder.name
verify(folder / image_name, folder / (image_name + ".manifest.json"))
export.mkdir(parents=True, exist_ok=True)
if destination != folder:
shutil.copytree(folder, destination, dirs_exist_ok=True)
verify(destination / image_name, destination / (image_name + ".manifest.json"))
print(f"[firmware-build] Exported folder: {destination}")
return destination
def archive_build(image: Path, metadata_path: Path, output: Path = None,
extra_files=(), export: Path = None) -> dict:
"""Keep a byte-identical build snapshot, alongside the existing flat output."""
image, metadata_path = image.resolve(), metadata_path.resolve()
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
name = build_folder_name(metadata)
output = (output if output is not None else image.parent / "firmware").resolve()
output.mkdir(parents=True, exist_ok=True)
destination = output / name
# Seal and verify staged copies before publishing files in the build folder.
with tempfile.TemporaryDirectory(prefix=".archive-", dir=output) as temporary:
stage = Path(temporary)
shutil.copy2(image, stage / image.name)
for file in extra_files:
file = Path(file)
shutil.copy2(file, stage / file.name)
shutil.copy2(metadata_path, stage / "firmware_identity.json")
seal(stage / image.name, stage / "firmware_identity.json")
verify(stage / image.name, stage / (image.name + ".manifest.json"))
destination.mkdir(parents=True, exist_ok=True)
for file in stage.iterdir():
file.replace(destination / file.name)
print(f"[firmware-build] Folder: {destination}")
result = {"directory": str(destination), "file": image.name}
if export is not None:
result["exported_directory"] = str(export_build(destination, export, image.name))
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
@@ -114,9 +186,14 @@ def main():
p = sub.add_parser("verify")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--manifest", type=Path, required=True)
p = sub.add_parser("archive")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--metadata", dest="metadata_path", type=Path, required=True)
p.add_argument("--output", type=Path)
p.add_argument("--export", type=Path)
args = vars(parser.parse_args())
command = args.pop("command")
result = {"prepare": prepare, "seal": seal, "verify": verify}[command](**args)
result = {"prepare": prepare, "seal": seal, "verify": verify, "archive": archive_build}[command](**args)
print(json.dumps(result, ensure_ascii=False))

View File

@@ -1,6 +1,6 @@
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки.
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки.
cmake_minimum_required(VERSION 3.13)
project(protocan_boot C)
@@ -17,31 +17,10 @@ else()
target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
if(PCAN_BOOT_BUILD_TESTS)
enable_testing()
add_executable(test_pcan_boot tests/test_pcan_boot.c)
target_link_libraries(test_pcan_boot PRIVATE protocan_boot)
add_test(NAME pcan_boot COMMAND test_pcan_boot)
endif()
if(PCAN_BOOT_BUILD_TESTS)
add_executable(test_f28335_can_boot tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c)
target_compile_definitions(test_f28335_can_boot PRIVATE PCAN_BOOT_WIDE_BYTES)
target_include_directories(test_f28335_can_boot PRIVATE tests/f28335_stubs)
add_test(NAME f28335_can_boot COMMAND test_f28335_can_boot)
endif()
if(PCAN_BOOT_BUILD_TESTS)
add_executable(test_f28335_application tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c)
target_compile_definitions(test_f28335_application PRIVATE PCAN_BOOT_WIDE_BYTES F28335_BOOT_LOADER=0)
target_include_directories(test_f28335_application PRIVATE tests/f28335_stubs)
add_test(NAME f28335_application COMMAND test_f28335_application)
foreach(width native wide)
add_executable(test_f28335_store_${width} tests/test_f28335_boot_store.c ports/tms320f28335/f28335_boot_store.c)
if(width STREQUAL "wide")
target_compile_definitions(test_f28335_store_${width} PRIVATE PCAN_BOOT_WIDE_BYTES)
endif()
add_test(NAME f28335_store_${width} COMMAND test_f28335_store_${width})
endforeach()
endif()

View File

@@ -97,8 +97,7 @@ typedef struct {
} pcan_boot_port_t;
```
Обязательны `send`, `erase_slot`, `write_slot`; в A/B также `set_pending_slot`.
В single-slot этот callback необязателен и может завершать активацию образа.
Обязательны `send`, `erase_slot`, `write_slot` и `set_pending_slot`.
`authorize` проверяет политику до стирания (тип, версия, anti-rollback).
`verify_image` выполняет платформенную проверку подписанного контейнера после
CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки
@@ -176,12 +175,5 @@ ctest --test-dir build --output-on-failure
- включить watchdog и rollback при неподтверждённом запуске;
- согласовать фильтры CAN для `MsgType=0x9..0xD`.
Ядро поддерживает платформенные порты; особенности интеграции перечислены ниже.
## F28335 / большая EEPROM
[Порт M95M02](ports/tms320f28335/README.md) подключён в `tms_periph/periph_167`.
Использует отдельный Flash-загрузчик и два слота EEPROM по 130560 байт.
Реализованы readback/CRC, проверка SCI8, журнал метаданных, пробный запуск,
CONFIRM/откат, PING/discovery и сервисный паспорт. Первичная установка Flash-
загрузчика и выбор Flash boot описаны в README порта.
Проект пока не подключён ни к одной конкретной плате; это самостоятельный
шаблон для интеграции в будущие загрузчики приборов SET.

View File

@@ -63,10 +63,6 @@ BlockIndex = 0x0000…0xFFFF
| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
| `0x0C` | `PING` | 0 | отсутствует | любое |
| `0x0D` | `CAPABILITIES` | 0 | отсутствует | любое, при поддержке порта |
| `0x0E` | `DEVICE_NAME` | 1 | номер страницы имени | любое, при поддержке порта |
| `0x0F` | `MEMORY_INFO` | 1 | номер страницы памяти | любое, при поддержке порта |
### `BEGIN_IMAGE`
@@ -85,8 +81,7 @@ DATA[4..7] FirmwareVersion, uint32 little-endian
```
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
тип изделия и аппаратную ревизию. Проверка версии/anti-rollback выполняется
только при наличии соответствующей политики платформы (callback authorize).
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
## `BOOT_STATUS`
@@ -186,16 +181,3 @@ Boot metadata должна атомарно хранить:
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
7. Новое приложение после самопроверки выполняет `CONFIRM`.
## F28335 M95M02
Реализованы отдельный Flash-загрузчик, A/B по 130560 байт, журнал, пробный запуск
и откат. Подробная раскладка памяти, сервисных ответов и первичная установка:
[README порта](../ports/tms320f28335/README.md).
Общий discovery: Route=0, DeviceType=7, DeviceID=15, MsgType=D, Body=0, DLC=0.
Он только запрашивает IDENTIFY; операции изменения всегда адресные.
ENTER_BOOT может отложить ACK до готовности отдельного загрузчика после reset.
CONFIRM с ненулевой session может обрабатываться приложением без ENTER_BOOT.
`pcan_boot_poll` обслуживает задержку discovery и тайм-аут сессии, если он задан
портом. Старым портам с timeout=0 периодический вызов нужен только для discovery.

View File

@@ -11,13 +11,6 @@
#include <stddef.h>
#include <stdint.h>
/* C28x has 16-bit addressable chars; wire octets use the low eight bits. */
#if defined(__TMS320C28XX__) || defined(PCAN_BOOT_WIDE_BYTES)
typedef unsigned int pcan_boot_byte_t;
#else
typedef uint8_t pcan_boot_byte_t;
#endif
#ifdef __cplusplus
extern "C" {
#endif
@@ -48,11 +41,7 @@ typedef enum {
PCAN_BOOT_CMD_CONFIRM = 0x08U,
PCAN_BOOT_CMD_REBOOT = 0x09U,
PCAN_BOOT_CMD_ABORT = 0x0AU,
PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU,
PCAN_BOOT_CMD_PING = 0x0CU,
PCAN_BOOT_CMD_CAPABILITIES = 0x0DU,
PCAN_BOOT_CMD_DEVICE_NAME = 0x0EU,
PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU
PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU
} pcan_boot_command_t;
typedef enum {
@@ -77,8 +66,7 @@ typedef enum {
PCAN_BOOT_STATE_READY_TO_ERASE,
PCAN_BOOT_STATE_RECEIVING,
PCAN_BOOT_STATE_VERIFIED,
PCAN_BOOT_STATE_FAILED,
PCAN_BOOT_STATE_COMMITTED
PCAN_BOOT_STATE_FAILED
} pcan_boot_state_t;
typedef struct {
@@ -86,47 +74,36 @@ typedef struct {
uint32_t image_crc32;
uint32_t firmware_version;
uint16_t product_type;
pcan_boot_byte_t hardware_revision_min;
pcan_boot_byte_t hardware_revision_max;
uint8_t hardware_revision_min;
uint8_t hardware_revision_max;
} pcan_boot_manifest_t;
typedef struct {
pcan_boot_byte_t device_type;
pcan_boot_byte_t device_id;
uint8_t device_type;
uint8_t device_id;
uint16_t product_type;
pcan_boot_byte_t hardware_revision;
uint8_t hardware_revision;
uint32_t firmware_version;
pcan_boot_byte_t active_slot;
uint8_t active_slot;
/** 1 for an in-place/single-slot target, 2 for A/B. Zero keeps legacy A/B. */
pcan_boot_byte_t slot_count;
uint8_t slot_count;
/** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */
uint32_t max_image_size;
pcan_boot_byte_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
uint32_t session_timeout_ms; /**< 0 disables timeout for legacy ports. */
uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
} pcan_boot_config_t;
typedef struct {
bool (*send)(void *user, uint32_t can_id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc);
bool (*erase_slot)(void *user, pcan_boot_byte_t slot);
bool (*write_slot)(void *user, pcan_boot_byte_t slot, uint32_t offset,
const pcan_boot_byte_t *data, pcan_boot_byte_t length);
bool (*send)(void *user, uint32_t can_id, const uint8_t *data, uint8_t dlc);
bool (*erase_slot)(void *user, uint8_t slot);
bool (*write_slot)(void *user, uint8_t slot, uint32_t offset,
const uint8_t *data, uint8_t length);
bool (*authorize)(void *user, const pcan_boot_manifest_t *manifest);
bool (*verify_image)(void *user, pcan_boot_byte_t slot,
bool (*verify_image)(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest);
/** A/B metadata, or optional single-slot activation after verification. */
bool (*set_pending_slot)(void *user, pcan_boot_byte_t slot,
bool (*set_pending_slot)(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest);
bool (*confirm_running_slot)(void *user);
void (*reboot)(void *user);
/** ENTER: 0 ready, 1 deferred ACK after reset, 2 denied. NULL = legacy ready. */
unsigned int (*enter_boot)(void *user, pcan_boot_byte_t session);
bool (*can_reboot)(void *user);
/** Eight-octet service response, addressed by command + page. */
bool (*service_info)(void *user, pcan_boot_byte_t command,
pcan_boot_byte_t page, pcan_boot_byte_t *out);
/** Optional precise result, replacing legacy boolean verify_image. */
pcan_boot_status_t (*verify_status)(void *user, pcan_boot_byte_t slot,
const pcan_boot_manifest_t *manifest);
} pcan_boot_port_t;
typedef struct {
@@ -138,13 +115,11 @@ typedef struct {
uint32_t running_crc32;
uint32_t bytes_received;
uint16_t next_block;
pcan_boot_byte_t session_id;
pcan_boot_byte_t target_slot;
pcan_boot_byte_t last_status;
uint8_t session_id;
uint8_t target_slot;
uint8_t last_status;
bool have_image;
bool have_compat;
uint32_t now_ms, last_activity_ms, discovery_at;
bool discovery_pending;
} pcan_boot_t;
/** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */
@@ -159,19 +134,16 @@ void pcan_boot_abort(pcan_boot_t *boot);
* Возвращает true, если кадр принадлежал загрузочному сервису этого прибора.
*/
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc);
/** Call regularly with a wrapping monotonic millisecond clock. */
void pcan_boot_poll(pcan_boot_t *boot, uint32_t now_ms);
const uint8_t *data, uint8_t dlc);
/** Переносимые операции с 29-битным ProtoCAN ID. */
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
pcan_boot_byte_t msg_type, uint16_t msg_body);
pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id);
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body);
uint8_t pcan_boot_id_route(uint32_t can_id);
uint8_t pcan_boot_id_device_type(uint32_t can_id);
uint8_t pcan_boot_id_device_id(uint32_t can_id);
uint8_t pcan_boot_id_msg_type(uint32_t can_id);
uint16_t pcan_boot_id_msg_body(uint32_t can_id);
#ifdef __cplusplus

View File

@@ -1,156 +0,0 @@
# F28335 + M95M02: CAN, отдельный загрузчик и A/B
Реализация состоит из Flash-загрузчика `periph_167/Bootloader`, переносимого
`f28335_boot_store.c`, аппаратного `f28335_boot_hw.c` и адаптера
`f28335_can_boot.c`. Приложение подключает их через `Source/Internal/can_boot.c`.
В сборке `EEPROM_M95M02=0` CAN Boot отключён. Для установленной M95M02 — значение 1.
## Адрес и формат
Device Type **3**, Device ID **7**, имя **F28335-M95M02**, Product **0x2833**, HW **1**.
В приложении FW берётся из `firmware_version.h`, формат `major<<24 | minor<<16 | patch`;
для версии 1.1.2 это `0x01010002`. Загрузчик сообщает версию подтверждённого образа
из журнала, при отсутствии такого образа — 0. Версия кандидата передаётся в BEGIN_COMPAT.
CAN **250000 бит/с**, Extended ID, окно **16 блоков по 8 октетов**.
ENTER_BOOT для session=1: `0x13790102`, STATUS: `0x1B7C0102`.
Образ — SCI8 BIN (`AA 08`), чётная длина от 24 до **130560 байт** включительно.
Числа в протоколе little endian. Адреса внутри SCI8: сначала старшее 16-битное
слово, затем младшее; каждое слово little endian. В C28x `char` 16-битный,
поэтому `pcan_boot_byte_t` хранит октет в младших 8 битах.
## Разметка EEPROM, байтовые адреса
| Область | Начало | Конец включительно | Размер |
|---|---:|---:|---:|
| Журнал 0 | 0x00000 | 0x000FF | 256 |
| Журнал 1 | 0x00100 | 0x001FF | 256 |
| Slot A | 0x00200 | 0x1FFFF | 130560 |
| Slot B | 0x20000 | 0x3FDFF | 130560 |
| Резерв | 0x3FE00 | 0x3FEFB | 252 |
| Параметры | 0x3FEFC | 0x3FFFD | 258 |
| Последнее слово | 0x3FFFE | 0x3FFFF | 2 |
В каждой странице журнала первые 64 октета содержат magic `FAB2`, версию 1,
sequence, active/pending/attempted, два manifest, CRC32 первых 56 октетов и
commit marker в октетах 60..63. Сначала обнуляется marker запасной страницы,
затем записывается тело с проверкой чтением, последним — marker. Предыдущая
страница остаётся действующей до полной публикации новой. Выбор sequence
учитывает переполнение u32. После ошибки публикации журнал перечитывается;
ошибка чтения блокирует последующие записи.
## Сценарий обновления и отката
1. Приложение принимает ENTER_BOOT, останавливает управление нагрузкой, выставляет 4 мА и выдерживает
секундный STOP, затем сохраняет session в NOINIT RAM 0x50..0x57. Watchdog выполняет reset.
ACK ENTER_BOOT отправляет уже Flash-загрузчик; приложение ничего не стирает.
2. BEGIN_IMAGE/BEGIN_COMPAT проверяют размер, Product, HW и сессию. Неактивный
слот выбирается автоматически; при первом образе это A. ERASE инвалидирует
только его manifest. Полного стирания EEPROM не требуется.
3. Последовательные DATA_A/B записываются с немедленным readback. VERIFY
проверяет потоковый CRC32, CRC32 EEPROM и полную таблицу SCI8.
Разрешена загрузка только в CPU word RAM `[0x8000,0xE000)`: максимум 64 секции,
без пересечений, усечений и хвостовых данных; entry должен попасть в секцию.
Запись в Flash, периферию, стек, RAM загрузчика и handoff запрещена.
4. COMMIT повторно проверяет образ и публикует pending в журнале. Подтверждённый
слот сохраняется. Повтор COMMIT безопасен. REBOOT выполняется после физического
CAN ACK ответа. Потеря ACK/тайм-аут передачи отменяет запрос reset.
5. Перед первым запуском кандидата загрузчик сохраняет attempted=1, затем
проверяет и копирует SCI8 в RAM. Следующий reset без CONFIRM сбрасывает pending
и запускает прежний active. Если прежнего образа нет — остаётся CAN recovery.
6. Новое приложение включает watchdog в начале запуска. До подтверждения
прерывания не кормят watchdog вместо основного цикла. После READY, 100 проходов
main и 5 секунд выполняется автоматический CONFIRM. Неудача сохранения
подтверждения через 10 секунд прекращает кормление watchdog, вызывая откат.
Адресная команда CONFIRM с ненулевой session также разрешена после READY.
CONFIRM в загрузчике и до первого запуска кандидата отклоняется.
PING, IDENTIFY и паспорт доступны без ENTER_BOOT. QUERY_PROGRESS/ABORT привязаны
к сессии. Сессия в RAM истекает через 30 секунд без подходящих команд/данных.
После reset передачу начинают заново; параметры и подтверждённый образ остаются.
ABORT после COMMIT не отменяет опубликованный pending. Для его пробного запуска
можно открыть новую сессию и отправить REBOOT. Pending запрещено перезаписывать.
Повторы ENTER_BOOT, метаданных, ERASE до первого блока, VERIFY и COMMIT учитывают
потерю ответа. Broadcast никогда не разрешает запись.
## Сервисные команды
Запрос: MsgType=9, Body=`session<<8 | command`. IDENTIFY/PING/паспорт не требуют сессии.
Ответ паспорта: MsgType=D, Body=`page<<8 | command`, DLC=8.
| Код | Имя | DLC запроса | Ответ |
|---:|---|---:|---|
| 01 | IDENTIFY | 0 | Product:u16, HW:u8, protocol:u8, FW:u32; Body=1 |
| 0C | PING | 0 | protocol, state, active, target, session, slot_count, last_status, 0 |
| 0D | CAPABILITIES | 0 | flags:u32, slot_capacity:u32 |
| 0E | DEVICE_NAME | 1 (page) | 8 ASCII-октетов имени; последний фрагмент с NUL |
| 0F | MEMORY_INFO | 1 (page) | Страницы ниже |
MEMORY_INFO 0: EEPROM bytes:u32, page bytes:u16, mode:u8 (0 app/1 boot), running:u8.
Страница 1: адреса A:u32, B:u32. Страница 2: active, pending, attempted, storage_healthy,
session_timeout_ms:u32. Страница 3: parameters_offset:u32, parameters_bytes:u32.
Слот FF означает отсутствие образа/запущенного приложения.
CAPABILITIES bits 0..11: ping, discovery, identify, name, memory, A/B, confirm,
rollback, separate loader, session timeout, readback, SCI8 validation.
Приложение, запущенное старым SPI ROM без handoff загрузчика, сообщает только
базовые сервисы (0x1F) и отклоняет ENTER_BOOT.
Обнаружение: MsgType=D, Body=0, DLC=0, Route=0. Адресный запрос или общий
DeviceType=7/DeviceID=15. Ответ IDENTIFY приходит с адреса прибора через
`2*(1+16*DeviceType+DeviceID)` мс. При нескольких одинаковых адресах конфликт
остаётся: каждому физическому устройству нужен уникальный адрес.
Python API: `protocan.can_boot.CanBootServices` — запросы и разбор паспорта;
`CanBootTransfer` — загрузка. Кнопка «Пинг» доступна в режиме ProtoCAN Boot
окна прошивки SETGUI. Для ENTER/VERIFY/COMMIT клиент ждёт до 15 секунд.
## Аппаратная интеграция
MBOX3 — адресные входящие Boot; MBOX4 — ответы; MBOX6 — точный broadcast discovery.
MBOX0..2 сохраняют legacy/UMP, MBOX5 остаётся heartbeat. ISR помещает сообщения
в очередь на 31 кадр. Чужие адреса, RTR и неподходящие типы отбрасываются до очереди.
При переполнении/потере mailbox сессия отменяется; повторная запись начинается заново.
SPI-драйвер загрузчика имеет ограничение ожидания байта и WIP, 24-битный адрес,
разбиение по страницам 256 байт и восстановление конфигурации SPI приложения.
Он работает только при остановленном Timer2. Обычный Seeprom_write приложения
ограничен областью параметров; старый RS EXTEND 0x44 запрещён для A/B.
Загрузчик исполняется из внутренней Flash A 0x338000..0x33FF7F, entry 0x33FFF6.
Его RAM — 0xE000..0xFFFF; стек — 0x400..0x7FF. Приложение M95M02 использует
0x8000..0xD7FF для кода, 0xD800..0xDFFF для констант/инициализации,
0xE000..0xFFFF для .ebss после перехода в C runtime. Linker резервирует
NOINIT handoff в обоих проектах. CAN не содержит операций записи внутренней Flash.
## Первичная установка
1. Собрать приложение с EEPROM_M95M02=1 и обновлённым F28335.cmd; получить SCI8 BIN.
Старые BIN/OUT автоматически не заменены. Приложение должно содержать интеграцию
READY/CONFIRM, иначе на следующем reset оно останется неподтверждённым кандидатом.
2. Собрать отдельный загрузчик TI C2000 COFF compiler:
`Bootloader/build.ps1 -CompilerRoot <путь-к-ti-cgt-c2000>`.
Результат — `Bootloader/build/F28335_CAN_AB.out` и MAP.
3. Один раз установить этот OUT через JTAG и выбрать аппаратный Flash boot
согласно схеме платы. Не загружать поверх него старый application OUT с
секцией codestart во Flash.
4. После reset пустая EEPROM или старая разметка автоматически ведут в CAN recovery.
Передать новое приложение через ProtoCAN Boot, дождаться пробного запуска и
проверить MEMORY_INFO page 2: pending=FF, active=A. Затем проверить обновление в B.
Старая SPI-ROM разметка с AA08 по адресу 0 несовместима с новым журналом.
Первое обновление заменяет её; параметры по прежнему адресу сохраняются.
Загрузчик не выполняет скрытую миграцию и не меняет boot pins программно.
## Проверки и границы проверенного
`python c/protocan-boot/tests/run_tests.py` (переменная CC — clang/gcc) проверяет
ядро, приложение/загрузчик, wide-octet режим, журнал, обрыв публикации на каждом
из 69 возможных положений, пробный запуск, CONFIRM, откат, SCI8 и границы памяти.
Python-тесты проверяют клиентские запросы, паспорт, фильтрацию ответов и лимит A/B.
Проверка синтаксиса использует настоящие DSP2833x headers. Сборка TI/CCS,
раскладка итогового MAP и физические CAN/SPI/reset на плате ещё не подтверждены.
CRC32 защищает от повреждения, криптографической подписи и anti-rollback по
версии в этом порту нет. Удалённый CONFIRM подтверждает готовность приложения,
но не заменяет проверку датчиков/силовой части на стенде.
Аппаратный формат SPI boot описан в [TI Boot ROM Reference Guide](https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/171/2742.Boot-ROM.pdf).
В A/B-схеме ROM выбирает Flash, а разбор EEPROM выполняет этот загрузчик.

View File

@@ -1,181 +0,0 @@
/* Shared by the application and the separate Flash bootloader. */
/* Аппаратный слой: общая RAM между reset, watchdog и SPI EEPROM.
* Один исходник компилируется и в приложение, и в отдельный Flash-загрузчик.
* В отличие от store.c этот файл требует настоящих регистров F28335. */
#include "DSP2833x_Device.h"
#include "f28335_boot_platform.h"
#include "f28335_can_boot.h"
/* Must be NOINIT in BOTH linkers, outside ROM scratch and application payload. */
#pragma DATA_SECTION(boot_handoff,".boot_shared")
/* 8 слов NOINIT по адресу 0x50, одинаково зарезервированных обоими linker-файлами.
* [0..1] magic; [2] запрошенная session; [3] запущенный слот; [4..6] служебные
* поля; [7] XOR-контроль. Это кратковременная передача контекста через watchdog
* reset, а не замена энергонезависимого журнала EEPROM. */
static volatile unsigned int boot_handoff[8];
/* Проверяем magic и XOR прежде, чем трактовать RAM как запрос входа в boot.
* После включения питания содержимое NOINIT может быть произвольным. */
static unsigned int handoff_valid(void)
{
unsigned int i,sum=0;
for(i=0;i<8;++i) sum^=boot_handoff[i];
return boot_handoff[0]==0xB283 && boot_handoff[1]==0x4D7C && sum==0xA95B;
}
/* Сначала снимаем признак действительности, затем заполняем поля и контроль.
* Первый magic записываем последним; volatile сохраняет обращения к общей RAM.
* request=0 означает обычный запуск, ненулевая session — явный ENTER_BOOT. */
static void handoff_set(unsigned int request,unsigned int running)
{
unsigned int i,sum=0;
boot_handoff[0]=0; boot_handoff[1]=0x4D7C;
boot_handoff[2]=request; boot_handoff[3]=running;
boot_handoff[4]=0xF335; boot_handoff[5]=0x0CCA; boot_handoff[6]=1;
sum=0xB283;
for(i=1;i<7;++i) sum^=boot_handoff[i];
boot_handoff[7]=sum^0xA95B;
boot_handoff[0]=0xB283; /* publish last */
}
/* Приложение узнаёт свой слот из handoff, а не предполагает, что оно active:
* во время пробного запуска выполняется pending, active ещё указывает на старый образ. */
unsigned int F28335Boot_RunningSlot(void)
{ return handoff_valid()?boot_handoff[3]:F28335_AB_NONE; }
unsigned int F28335Boot_RequestedSession(void)
{ return handoff_valid() && boot_handoff[2]<256?boot_handoff[2]:0; }
/* Одновременно публикуем выбранный слот и потребляем запрос ENTER_BOOT:
* он не должен повторяться при следующем обычном reset. */
void F28335Boot_SetRunning(unsigned int slot) { handoff_set(0,slot); }
/* Timer1 должен давать 1 мс на InterruptCount и в приложении, и в загрузчике.
* Тайм-ауты используют разность unsigned-счётчиков для переноса через wrap. */
uint32_t F28335Boot_Now(void) { return CpuTimer1.InterruptCount; }
/* Вызвать сразу после InitSysCtrl, который штатно отключает watchdog.
* Сбрасываем разрешение кормления из ISR до полной инициализации CAN-сервиса.
* Для приложения, запущенного нашим загрузчиком, включаем watchdog с делителем. */
void F28335Boot_StartupWatchdog(void)
{
F28335CanBoot_EarlyInit();
if(F28335Boot_RunningSlot()<2) {
EALLOW; SysCtrlRegs.WDKEY=0x55; SysCtrlRegs.WDKEY=0xAA;
SysCtrlRegs.WDCR=0x002F; EDIS;
}
}
/* Платформенный Quiesce останавливает управление выходами перед reset.
* Session сохраняется до включения watchdog; ненулевую session загрузчик
* использует для отложенного ACK ENTER_BOOT. В бесконечном цикле watchdog
* намеренно не обслуживается — выход из функции должен произойти через reset. */
void F28335Boot_Reset(unsigned int session)
{
F28335Boot_Quiesce();
DINT;
handoff_set(session,F28335_AB_NONE);
EALLOW; SysCtrlRegs.WDCR=0x0028; EDIS;
for(;;) { }
}
/* Main context only. Legacy Timer2 EEPROM driver must be idle/stopped. Save
* its SPI configuration: it changes the character width during transactions. */
typedef struct { unsigned int ccr,ctl,brr,tx,rx,ct,pri; } boot_spi_saved;
static void spi_cs(unsigned int high)
{ if(high) GpioDataRegs.GPASET.bit.GPIO19=1; else GpioDataRegs.GPACLEAR.bit.GPIO19=1; }
/* Доступ разрешён только из main при остановленном Timer2 старого SPI-драйвера.
* Это проверка занятости в однопоточном приложении, не универсальный mutex.
* Сохраняем режим SPI, поскольку штатный драйвер меняет разрядность передачи.
* CS сначала поднимаем, затем переключаем SPI на 8-битные транзакции. */
static bool spi_take(boot_spi_saved *s)
{
volatile unsigned int discard;
if(!CpuTimer2Regs.TCR.bit.TSS) return false;
s->ccr=SpiaRegs.SPICCR.all; s->ctl=SpiaRegs.SPICTL.all; s->brr=SpiaRegs.SPIBRR;
s->tx=SpiaRegs.SPIFFTX.all; s->rx=SpiaRegs.SPIFFRX.all;
s->ct=SpiaRegs.SPIFFCT.all; s->pri=SpiaRegs.SPIPRI.all;
EALLOW;
GpioCtrlRegs.GPAMUX2.bit.GPIO16=1; GpioCtrlRegs.GPAMUX2.bit.GPIO17=1;
GpioCtrlRegs.GPAMUX2.bit.GPIO18=1; GpioCtrlRegs.GPAMUX2.bit.GPIO19=0;
GpioDataRegs.GPASET.bit.GPIO19=1; GpioCtrlRegs.GPADIR.bit.GPIO19=1;
GpioCtrlRegs.GPAQSEL2.bit.GPIO17=3;
EDIS;
SpiaRegs.SPICCR.all=7; SpiaRegs.SPICTL.all=0x0E; /* master, phase=1, IRQ off */
SpiaRegs.SPIBRR=CLKMULT*6+1;
SpiaRegs.SPIFFTX.all=0x8000; SpiaRegs.SPIFFRX.all=0; SpiaRegs.SPIFFCT.all=0;
SpiaRegs.SPIPRI.all=0x10; SpiaRegs.SPICCR.all=0x87;
/* Снимаем оставшийся RX/INT_FLAG от прежней операции, чтобы первый обмен
* не принял старое содержимое RXBUF за ответ EEPROM. */
discard=SpiaRegs.SPIRXBUF; (void)discard;
return true;
}
/* Выход и при успехе, и при ошибке: CS не должен остаться активным.
* Настройки возвращаются при удержании SPI в reset; исходный SPISWRESET
* восстанавливается последним, после остальных регистров. */
static void spi_release(const boot_spi_saved *s)
{
volatile unsigned int discard;
spi_cs(1); discard=SpiaRegs.SPIRXBUF; (void)discard;
SpiaRegs.SPICCR.all=s->ccr&0x7FU; SpiaRegs.SPICTL.all=s->ctl;
SpiaRegs.SPIBRR=s->brr; SpiaRegs.SPIFFTX.all=s->tx; SpiaRegs.SPIFFRX.all=s->rx;
SpiaRegs.SPIFFCT.all=s->ct; SpiaRegs.SPIPRI.all=s->pri; SpiaRegs.SPICCR.all=s->ccr;
}
/* У F28335 8-битный символ для передачи выравнивается в старшие биты TXBUF.
* Полученный октет читается из младших битов RXBUF. Счётчик budget ограничивает
* ожидание даже при неработающем Timer1; это число итераций, а не миллисекунды. */
static bool spi_octet(unsigned int tx,unsigned int *rx)
{
uint32_t budget=100000UL;
SpiaRegs.SPITXBUF=(tx&255U)<<8;
while(!SpiaRegs.SPISTS.bit.INT_FLAG) if(--budget==0) return false;
*rx=SpiaRegs.SPIRXBUF&255U; return true;
}
/* Команда EEPROM и 24-битный байтовый адрес, старший октет адреса первым.
* CS удерживается вызывающей функцией на всю команду и полезные данные. */
static bool spi_prefix(unsigned int command,uint32_t address)
{
unsigned int discard;
return spi_octet(command,&discard) && spi_octet((unsigned int)(address>>16),&discard) &&
spi_octet((unsigned int)(address>>8),&discard) && spi_octet((unsigned int)address,&discard);
}
/* RDSR (05h): WIP в бите 0 означает незавершённую внутреннюю запись EEPROM.
* Ожидание ограничено и 50 мс, и числом опросов на случай остановки tick.
* После каждого чтения статуса CS снимается, в том числе при ошибке SPI. */
static bool spi_ready(void)
{
unsigned int status,discard; uint32_t started=F28335Boot_Now(),budget=20000UL;
do {
spi_cs(0);
if(!spi_octet(5,&discard) || !spi_octet(255,&status)) { spi_cs(1); return false; }
spi_cs(1);
if(!(status&1U)) return true;
} while(--budget && (uint32_t)(F28335Boot_Now()-started)<50UL);
return false;
}
/* Читать разрешено всю физическую EEPROM, включая параметры. Проверка
* через вычитание защищает от переполнения адреса. spi_release обязателен
* после успешного захвата SPI независимо от результата последующего обмена. */
bool F28335Boot_Read(void *u,uint32_t address,pcan_boot_byte_t *p,unsigned int n)
{
boot_spi_saved saved; unsigned int i,value; bool ok; (void)u;
if(address>0x40000UL || n>0x40000UL-address || !spi_take(&saved)) return false;
ok=spi_ready();
spi_cs(0); if(ok) ok=spi_prefix(3,address);
for(i=0;ok && i<n;++i) { ok=spi_octet(255,&value); p[i]=value; }
spi_release(&saved); return ok;
}
/* Нижний аппаратный барьер записи: разрешены журнал и слоты до 0x3FE00.
* Резерв, параметры и последнее слово нельзя изменить даже ошибочным вызовом
* этой функции. Защита активного слота дополнительно выполняется в store.c. */
bool F28335Boot_Write(void *u,uint32_t address,const pcan_boot_byte_t *p,unsigned int n)
{
boot_spi_saved saved; unsigned int i,chunk,discard; bool ok; (void)u;
/* This driver cannot overwrite parameters, even if a caller is defective. */
if(address>0x3FE00UL || n>0x3FE00UL-address || !spi_take(&saved)) return false;
ok=spi_ready();
while(ok && n) {
/* Нельзя пересекать страницу одной командой WRITE: EEPROM может завернуть
* адрес внутри страницы. Для каждого фрагмента заново выдаём WREN (06h),
* WRITE (02h) и ждём снятия WIP. Сравнение readback выполняет верхний слой. */
chunk=256U-(unsigned int)(address&255U); if(chunk>n) chunk=n;
spi_cs(0); ok=spi_octet(6,&discard); spi_cs(1); /* WREN */
spi_cs(0); if(ok) ok=spi_prefix(2,address);
for(i=0;ok && i<chunk;++i) ok=spi_octet(p[i],&discard);
spi_cs(1); if(ok) ok=spi_ready();
n-=chunk; p+=chunk; address+=chunk;
}
spi_release(&saved); return ok;
}

View File

@@ -1,17 +0,0 @@
#ifndef F28335_BOOT_PLATFORM_H
#define F28335_BOOT_PLATFORM_H
#include "f28335_boot_store.h"
#ifndef F28335_BOOT_LOADER
#define F28335_BOOT_LOADER 0
#endif
bool F28335Boot_Read(void *u,uint32_t address,pcan_boot_byte_t *p,unsigned int n);
bool F28335Boot_Write(void *u,uint32_t address,const pcan_boot_byte_t *p,unsigned int n);
uint32_t F28335Boot_Now(void);
void F28335Boot_Reset(unsigned int session);
unsigned int F28335Boot_RunningSlot(void);
unsigned int F28335Boot_RequestedSession(void);
void F28335Boot_SetRunning(unsigned int slot);
void F28335Boot_StartupWatchdog(void);
/* Application hook must stop outputs before reset. No EEPROM operations here. */
void F28335Boot_Quiesce(void);
#endif

View File

@@ -1,254 +0,0 @@
/* Хранилище A/B без зависимости от регистров F28335.
* Здесь находятся правила сохранения образа при сбое питания и выбора слота.
* CAN отвечает за порядок команд; этот слой повторно защищает active/pending
* и границы EEPROM, даже если вызывающий код передал неверные параметры.
* Адрес EEPROM измеряется в октетах, адрес назначения SCI8 — в словах CPU. */
#include "f28335_boot_store.h"
#include <string.h>
/* Формат журнала фиксирован: u32 little endian. Нельзя сохранять C-структуру
* через memcpy: выравнивание и sizeof на C28x отличаются от обычного ПК. */
static uint32_t bs_get32(const pcan_boot_byte_t *p)
{ return (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24); }
static void bs_put32(pcan_boot_byte_t *p, uint32_t v)
{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(8*i))&255U); }
/* Потоковый CRC32: первый вызов получает crc=0, следующие — предыдущий результат.
* Внутренняя инверсия позволяет считать один CRC по нескольким порциям EEPROM.
* Маска 255 исключает старшие биты 16-битного представления октета на C28x.
* Это проверка повреждения данных, а не криптографическая подпись. */
uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n)
{ unsigned int i,b; crc=~crc; for(i=0;i<n;++i) { crc^=p[i]&255U;
for(b=0;b<8;++b) crc=(crc>>1)^((crc&1U)?0xEDB88320UL:0); } return ~crc; }
static uint32_t bs_base(unsigned int slot) { return slot?F28335_AB_SLOT_B:F28335_AB_SLOT_A; }
/* Принимаем запись только при совпадении magic, версии, marker и CRC.
* Дополнительно проверяем смысл номеров слотов: pending не может быть active.
* Наличие правильного marker без правильного CRC не разрешает запуск. */
static bool bs_record_valid(const pcan_boot_byte_t *p)
{
return bs_get32(p)==0x32424146UL && bs_get32(p+4)==1 &&
bs_get32(p+60)==0x51AFC03DUL && bs_get32(p+56)==f28335_boot_crc(0,p,56) &&
(p[12]<2 || p[12]==F28335_AB_NONE) &&
(p[13]<2 || p[13]==F28335_AB_NONE) && p[14]<=1 &&
(p[13]==F28335_AB_NONE || p[13]!=p[12]);
}
/* Карта записи, смещения в октетах:
* 0..3 magic FAB2; 4..7 версия; 8..11 sequence; 12 active; 13 pending;
* 14 attempted; 15 резерв; 16..35 manifest A; 36..55 manifest B;
* 56..59 CRC первых 56 октетов; 60..63 marker завершённой публикации.
* Manifest: size, CRC, FW (по u32), Product (u16), HW min/max, 4 октета резерва. */
static void bs_decode(f28335_boot_store_t *s, const pcan_boot_byte_t *p)
{
unsigned int i; s->sequence=bs_get32(p+8); s->active=p[12];
s->pending=p[13]; s->attempted=p[14];
for(i=0;i<2;++i) {
const pcan_boot_byte_t *q=p+16+i*20;
s->image[i].image_size=bs_get32(q); s->image[i].image_crc32=bs_get32(q+4);
s->image[i].firmware_version=bs_get32(q+8);
s->image[i].product_type=(uint16_t)(q[12]|(q[13]<<8));
s->image[i].hardware_revision_min=q[14]; s->image[i].hardware_revision_max=q[15];
}
}
/* Читаем обе копии журнала. Нераспознанная разметка допускает CAN recovery
* с active=NONE; ошибка чтения запрещает запись, поскольку реальный active неизвестен.
* Эта функция не преобразует старый SCI8 по адресу 0 в новую A/B-разметку. */
bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io)
{
pcan_boot_byte_t a[64],b[64]; bool va,vb;
if(!s || !io || !io->read || !io->write) return false;
memset(s,0,sizeof(*s)); s->io=*io; s->active=s->pending=F28335_AB_NONE;
if(!io->read(io->user,0,a,64) || !io->read(io->user,256,b,64)) { s->io_fault=true; return false; }
va=bs_record_valid(a); vb=bs_record_valid(b);
if(!va && !vb) return true;
/* Сравнение sequence по модулю 2^32: разность менее половины диапазона
* считается более новой записью. При равенстве выбирается страница 1.
* Соседние публикации отличаются на единицу, в том числе после переполнения. */
s->page=vb && (!va || (uint32_t)(bs_get32(b+8)-bs_get32(a+8))<0x80000000UL)?1:0;
bs_decode(s,s->page?b:a); s->valid=true; return true;
}
/* Публикация выполняется в другую страницу, текущую действующую не трогаем.
* Порядок принципиален: обнулить marker -> проверить -> записать тело ->
* проверить тело -> записать marker последним -> проверить всю запись.
* При обрыве питания загрузчик выбирает прежнюю либо полностью новую запись.
* Каждый memcmp учитывает sizeof элемента, поскольку элемент здесь — октет
* протокола, но на C28x занимает адресуемое 16-битное слово. */
static bool bs_save(f28335_boot_store_t *s)
{
pcan_boot_byte_t p[64],check[64],invalid[4]={0,0,0,0}; unsigned int i;
unsigned int page=s->valid?(s->page^1U):0;
uint32_t addr=(uint32_t)page*256;
memset(p,0,sizeof(p)); bs_put32(p,0x32424146UL); bs_put32(p+4,1);
bs_put32(p+8,s->sequence+1); p[12]=s->active; p[13]=s->pending; p[14]=s->attempted;
for(i=0;i<2;++i) {
pcan_boot_byte_t *q=p+16+i*20;
bs_put32(q,s->image[i].image_size); bs_put32(q+4,s->image[i].image_crc32);
bs_put32(q+8,s->image[i].firmware_version);
q[12]=s->image[i].product_type&255U; q[13]=(s->image[i].product_type>>8)&255U;
q[14]=s->image[i].hardware_revision_min; q[15]=s->image[i].hardware_revision_max;
}
bs_put32(p+56,f28335_boot_crc(0,p,56)); bs_put32(p+60,0x51AFC03DUL);
/* The old page is never erased. A torn write cannot publish a partial record. */
if(!s->io.write(s->io.user,addr+60,invalid,4) ||
!s->io.read(s->io.user,addr+60,check,4) || memcmp(check,invalid,sizeof(invalid)) ||
!s->io.write(s->io.user,addr,p,60) ||
!s->io.read(s->io.user,addr,check,60) || memcmp(p,check,60*sizeof(*p)) ||
!s->io.write(s->io.user,addr+60,p+60,4) ||
!s->io.read(s->io.user,addr,check,64) || memcmp(p,check,sizeof(p))) return false;
s->page=page; s->sequence++; s->valid=true; return true;
}
/* Изменения сначала готовятся в копии состояния next. Рабочее состояние
* заменяем только после публикации. Если запись/контрольное чтение сорвались,
* перечитываем журнал: marker мог успеть записаться до ошибки readback.
* После неудачи чтения io_fault блокирует дальнейшие изменения хранилища. */
static bool bs_publish(f28335_boot_store_t *s, f28335_boot_store_t *next)
{
if(bs_save(next)) { *s=*next; return true; }
/* A failed final read may still have committed: reload before any more writes. */
{ f28335_boot_io_t io=s->io; if(!f28335_boot_open(s,&io)) {
s->valid=false; s->io_fault=true; return false; } }
return false;
}
/* Логический ERASE: обнуляем manifest неактивного слота в журнале.
* Саму EEPROM стирать целиком не требуется — каждый байт будет перезаписан.
* Active защищён всегда; наличие pending запрещает начало новой записи,
* пока кандидат не подтверждён либо не снят при следующем выборе загрузки. */
bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot)
{
f28335_boot_store_t next=*s;
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE) return false;
memset(&next.image[slot],0,sizeof(next.image[slot]));
return bs_publish(s,&next);
}
/* Записываем не более одного CAN-блока и немедленно сравниваем прочитанное.
* Проверка n <= capacity-off выполняется после off <= capacity: это исключает
* переполнение суммы off+n и выход из слота в соседний слот или параметры. */
bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t off,
const pcan_boot_byte_t *p, unsigned int n)
{
pcan_boot_byte_t check[8];
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE || !n || n>8 ||
off>F28335_AB_SLOT_BYTES || n>F28335_AB_SLOT_BYTES-off) return false;
return s->io.write(s->io.user,bs_base(slot)+off,p,n) &&
s->io.read(s->io.user,bs_base(slot)+off,check,n) && !memcmp(check,p,n*sizeof(*p));
}
/* Слово SCI8 занимает два октета файла, младший октет идёт первым.
* Смещение увеличиваем только после успешного чтения; усечённый файл отклоняем. */
static bool bs_u16(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint16_t *v)
{
pcan_boot_byte_t p[2]; if(*off>size || size-*off<2 ||
!s->io.read(s->io.user,base+*off,p,2)) return false;
*v=(uint16_t)(p[0]|(p[1]<<8)); *off+=2; return true;
}
/* Особенность таблицы TI: адрес u32 передаётся старшим словом вперёд,
* при этом внутри каждого слова остаётся little endian. Не заменять на get32. */
static bool bs_addr(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint32_t *v)
{
uint16_t hi,lo; if(!bs_u16(s,base,size,off,&hi) || !bs_u16(s,base,size,off,&lo)) return false;
*v=((uint32_t)hi<<16)|lo; return true;
}
/* The complete table is validated before any application RAM is written.
* Loader RAM is E000..FFFF and M0/M1. Flash, peripherals and XINTF are forbidden. */
/* writer=NULL: только разбираем и проверяем таблицу, RAM приложения не меняем.
* writer задан: повторно проверяем таблицу и переносим слова в разрешённую RAM.
* Массивы starts/ends ограничены MAX_SECTIONS; они нужны для обнаружения
* пересекающихся секций, в том числе секций с разным порядком адресов. */
static bool bs_table(f28335_boot_store_t *s,unsigned int slot,uint32_t size,
f28335_boot_ram_writer writer,void *user,uint32_t *entry)
{
uint32_t starts[F28335_AB_MAX_SECTIONS],ends[F28335_AB_MAX_SECTIONS];
uint32_t off=0,addr,end,base=bs_base(slot); unsigned int sections=0,i;
uint16_t count,word; bool entry_loaded=false;
if(!bs_u16(s,base,size,&off,&word) || word!=0x08AA) return false;
/* После ключа AA 08 пропускаем 16 резервных октетов; далее расположен entry.
* Нулевой count завершает таблицу строго на конце файла, без хвостовых данных. */
off=18; if(!bs_addr(s,base,size,&off,entry)) return false;
for(;;) {
if(!bs_u16(s,base,size,&off,&count)) return false;
if(!count) return off==size && entry_loaded;
if(sections==F28335_AB_MAX_SECTIONS || !bs_addr(s,base,size,&off,&addr)) return false;
end=addr+count;
/* Разрешён только полуинтервал RAM [0x8000,0xE000) в словах CPU.
* Верхняя граница защищает RAM работающего загрузчика; нижняя — стек,
* handoff и регистры. Проверка размера payload использует уже октеты файла. */
if(addr<0x8000UL || end>0xE000UL || end<=addr ||
off>size || (uint32_t)count*2>size-off) return false;
for(i=0;i<sections;++i) if(addr<ends[i] && end>starts[i]) return false;
starts[sections]=addr; ends[sections++]=end;
if(*entry>=addr && *entry<end) entry_loaded=true;
if(writer) { for(i=0;i<count;++i) {
if(!bs_u16(s,base,size,&off,&word) || !writer(user,addr+i,word)) return false;
} } else off+=(uint32_t)count*2;
}
}
/* VERIFY проверяет содержимое самой EEPROM, а не только CRC принятого потока.
* Затем проверяется структура SCI8 и попадание entry в загружаемую секцию.
* Выходной entry необязателен: COMMIT нужна проверка, но не адрес перехода. */
bool f28335_boot_verify(f28335_boot_store_t *s,unsigned int slot,
const pcan_boot_manifest_t *m,uint32_t *entry)
{
uint32_t off,crc=0,local_entry; unsigned int n; pcan_boot_byte_t p[32];
if(slot>1 || m->image_size<24 || m->image_size>F28335_AB_SLOT_BYTES ||
(m->image_size&1) || m->product_type!=0x2833 ||
m->hardware_revision_min>1 || m->hardware_revision_max<1) return false;
for(off=0;off<m->image_size;off+=n) {
n=(m->image_size-off<32)?(unsigned int)(m->image_size-off):32;
if(!s->io.read(s->io.user,bs_base(slot)+off,p,n)) return false;
crc=f28335_boot_crc(crc,p,n);
}
if(crc!=m->image_crc32 || !bs_table(s,slot,m->image_size,0,0,&local_entry)) return false;
if(entry) *entry=local_entry;
return true;
}
/* COMMIT публикует проверенный образ как pending, не меняя active.
* Повторная проверка EEPROM непосредственно перед публикацией не позволяет
* назначить кандидатом образ, испорченный после предыдущего VERIFY. */
bool f28335_boot_commit(f28335_boot_store_t *s,unsigned int slot,const pcan_boot_manifest_t *m)
{
f28335_boot_store_t next=*s;
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE ||
!f28335_boot_verify(s,slot,m,0)) return false;
next.image[slot]=*m; next.pending=slot; next.attempted=0;
return bs_publish(s,&next);
}
/* Подтверждать можно только действительно выбранный для пробного запуска слот.
* Требование attempted исключает подтверждение ещё ни разу не запускавшегося
* кандидата. Повтор CONFIRM уже активного слота возвращает успех без записи. */
bool f28335_boot_confirm(f28335_boot_store_t *s,unsigned int running_slot)
{
f28335_boot_store_t next=*s;
if(s->io_fault || running_slot>1) return false;
if(s->pending==F28335_AB_NONE) return s->valid && s->active==running_slot;
if(s->pending!=running_slot || !s->attempted) return false;
next.active=running_slot; next.pending=F28335_AB_NONE; next.attempted=0;
return bs_publish(s,&next);
}
/* Выбор выполняется при старте загрузчика. attempted сохраняется ДО перехода
* в приложение: после зависания, watchdog reset или потери питания следующий
* старт уже знает о незавершённой пробе и возвращает прежний active.
* Успех функции с slot=NONE означает recovery, а не разрешение перехода в RAM. */
bool f28335_boot_select(f28335_boot_store_t *s,unsigned int *slot)
{
f28335_boot_store_t next=*s; *slot=F28335_AB_NONE;
if(s->io_fault) return false;
if(s->pending!=F28335_AB_NONE) {
if(!s->attempted && f28335_boot_verify(s,s->pending,&s->image[s->pending],0)) {
next.attempted=1;
if(!bs_publish(s,&next)) return false;
*slot=s->pending; return true;
}
/* Кандидат уже запускался без подтверждения либо не прошёл проверку.
* Снимаем pending отдельной публикацией; подтверждённый active остаётся прежним. */
next.pending=F28335_AB_NONE; next.attempted=0;
if(!bs_publish(s,&next)) return false;
}
if(s->active<2 && f28335_boot_verify(s,s->active,&s->image[s->active],0)) *slot=s->active;
return true;
}
/* Сначала полная проверка CRC и всех адресов без записи, затем перенос слов.
* Платформа вызывает entry только при true; частичная загрузка при ошибке I/O
* не даёт разрешения на запуск приложения. */
bool f28335_boot_load(f28335_boot_store_t *s,unsigned int slot,
f28335_boot_ram_writer writer,void *user,uint32_t *entry)
{
return slot<2 && writer && entry && f28335_boot_verify(s,slot,&s->image[slot],entry) &&
bs_table(s,slot,s->image[slot].image_size,writer,user,entry);
}

View File

@@ -1,55 +0,0 @@
#ifndef F28335_BOOT_STORE_H
#define F28335_BOOT_STORE_H
#include "../../include/pcan_boot.h"
/* All EEPROM offsets/lengths below are OCTETS, CPU destinations are WORDS. */
/* Физическая EEPROM 256 КиБ: две страницы журнала по 256 октетов,
* два слота по 130560 октетов. С 0x3FE00 начинается резерв, с 0x3FEFC —
* прежняя область параметров. Константы ниже не являются CPU word-адресами. */
#define F28335_AB_SLOT_BYTES 0x1FE00UL
#define F28335_AB_SLOT_A 0x00200UL
#define F28335_AB_SLOT_B 0x20000UL
#define F28335_AB_PARAMS 0x3FEFCUL
#define F28335_AB_NONE 0xFFU
#define F28335_AB_RECORD 64U
#define F28335_AB_MAX_SECTIONS 64U
/* Контракт I/O: address и n измеряются в октетах, независимо от sizeof(char).
* true означает, что выполнена вся операция; короткий обмен должен вернуть false.
* Callbacks вызываются синхронно из main, а user передаётся им без изменений. */
typedef struct {
bool (*read)(void *, uint32_t, pcan_boot_byte_t *, unsigned int);
bool (*write)(void *, uint32_t, const pcan_boot_byte_t *, unsigned int);
void *user;
} f28335_boot_io_t;
typedef struct {
f28335_boot_io_t io;
/* sequence — номер последней публикации; page — выбранная копия журнала.
* active — подтверждённый слот; pending — кандидат; attempted — уже была проба.
* valid показывает наличие распознанной записи, io_fault запрещает изменения
* после ошибки чтения. Отсутствующий слот обозначается NONE, а не нулём. */
uint32_t sequence;
unsigned int page, active, pending, attempted;
bool valid, io_fault;
pcan_boot_manifest_t image[2];
} f28335_boot_store_t;
uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n);
bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io);
/* Blank/unrecognised EEPROM opens successfully with active=NONE. I/O errors fail. */
bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot);
bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t offset,
const pcan_boot_byte_t *data, unsigned int n);
bool f28335_boot_verify(f28335_boot_store_t *s, unsigned int slot,
const pcan_boot_manifest_t *m, uint32_t *entry);
bool f28335_boot_commit(f28335_boot_store_t *s, unsigned int slot,
const pcan_boot_manifest_t *m);
bool f28335_boot_confirm(f28335_boot_store_t *s, unsigned int running_slot);
/* Records a trial BEFORE returning it; a second unconfirmed boot rolls back. */
bool f28335_boot_select(f28335_boot_store_t *s, unsigned int *slot);
/* Запись одного проверенного слова приложения по CPU word-адресу.
* Должна вернуть false при невозможности записи; после отказа запуск запрещён. */
typedef bool (*f28335_boot_ram_writer)(void *, uint32_t, uint16_t);
bool f28335_boot_load(f28335_boot_store_t *s, unsigned int slot,
f28335_boot_ram_writer write_word, void *user, uint32_t *entry);
#endif

View File

@@ -1,276 +0,0 @@
/* Separate Flash recovery loader and application service adapter. */
#include "DSP2833x_Device.h"
#include "f28335_can_boot.h"
#if EEPROM_M95M02
#include "f28335_boot_platform.h"
#include <string.h>
/* boot хранит текущую CAN-сессию в RAM, store — прочитанное состояние журнала.
* Сброс сессии не удаляет active/pending из EEPROM. */
static pcan_boot_t boot;
static f28335_boot_store_t store;
/* Кольцевая очередь: ISR производит кадры, main потребляет. Из 32 элементов
* используется 31, чтобы head==tail однозначно означало пустую очередь.
* Долгие операции SPI запрещены внутри обработчика CAN. */
static volatile unsigned int head,tail,overflow;
static struct { uint32_t id; unsigned int dlc; pcan_boot_byte_t data[8]; } queue[32];
/* reset_requested только планирует reset; Service ждёт завершения TX.
* ready сообщает об окончании старта приложения, confirmed — о сохранённом
* подтверждении слота. healthy отражает результат открытия журнала при Init. */
static unsigned int reset_requested,reset_session,ready,healthy,confirmed;
static uint32_t sent_at,ready_at;
static unsigned int main_passes;
static int tx_busy,tx_canceling;
/* Один исходящий mailbox исключает перезапись ещё передаваемого ответа.
* MDL содержит первые четыре октета, MDH — следующие четыре, младшим вперёд.
* Запрос CANTRS ещё не означает успешную доставку: CANTA проверяется в Service.
* Аппаратный CAN ACK не является отдельным прикладным подтверждением от ПК. */
static bool transmit(void *u, uint32_t id, const pcan_boot_byte_t *d, pcan_boot_byte_t n)
{
unsigned int i; unsigned long lo=0, hi=0; (void)u;
if (tx_busy) return false;
for(i=0;i<n;++i) {
if(i<4) lo|=(unsigned long)(d[i]&255U)<<(8U*i);
else hi|=(unsigned long)(d[i]&255U)<<(8U*(i-4U));
}
EALLOW;
ECanaRegs.CANME.all &= ~16UL;
ECanaMboxes.MBOX4.MSGID.all=0x80000000UL|id;
ECanaMboxes.MBOX4.MSGCTRL.all=n;
ECanaMboxes.MBOX4.MDL.all=lo; ECanaMboxes.MBOX4.MDH.all=hi;
ECanaRegs.CANME.all |=16UL;
ECanaRegs.CANTA.all=16UL; ECanaRegs.CANAA.all=16UL;
ECanaRegs.CANTRS.all=16UL;
EDIS;
sent_at=F28335Boot_Now(); tx_busy=1; tx_canceling=0; return true;
}
/* Изменение образа разрешено только сборке Flash-загрузчика. В приложении
* эти callbacks возвращают отказ, даже если им удастся передать команду записи.
* ERASE для EEPROM означает инвалидирование manifest выбранного слота. */
static bool erase(void *u,pcan_boot_byte_t slot)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_prepare(&store,slot); }
static bool write_block(void *u,pcan_boot_byte_t slot,uint32_t offset,const pcan_boot_byte_t *d,pcan_boot_byte_t n)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_write(&store,slot,offset,d,n); }
/* Проверка включает CRC EEPROM и структуру SCI8. Сейчас отказ этого слоя
* кодируется CRC_ERROR как общий отказ проверки образа; причины внутри
* парсера SCI8 отдельными статусами протокола не различаются. */
static pcan_boot_status_t verify(void *u,pcan_boot_byte_t slot,const pcan_boot_manifest_t *m)
{
(void)u;
if(!F28335_BOOT_LOADER || !healthy) return PCAN_BOOT_STATUS_INVALID_STATE;
return f28335_boot_verify(&store,slot,m,0)?PCAN_BOOT_STATUS_OK:PCAN_BOOT_STATUS_CRC_ERROR;
}
static bool commit(void *u,pcan_boot_byte_t slot,const pcan_boot_manifest_t *m)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_commit(&store,slot,m); }
/* Подтверждение принадлежит приложению после READY. Загрузчик не может
* подтвердить кандидата сам: иначе исчезнет смысл проверки пробного запуска.
* Разрешение кормления watchdog из ISR открываем только после записи журнала. */
static bool confirm(void *u)
{
unsigned int running=F28335Boot_RunningSlot(); (void)u;
if(F28335_BOOT_LOADER || !healthy || !ready || running>1) return false;
if(!f28335_boot_confirm(&store,running)) return false;
boot.config.active_slot=running; confirmed=1; return true;
}
static bool can_reboot(void *u) { (void)u; return healthy; }
static void reboot(void *u) { (void)u; reset_session=0; reset_requested=1; }
/* Контракт enter_boot: 0 — загрузчик готов и ядро может отправить ACK;
* 1 — переход отложен, ACK придёт после reset; 2 — вход запрещён.
* Приложение требует handoff нашего загрузчика и подтверждённый текущий слот,
* чтобы не начать обновление из старого SPI-ROM старта или незавершённой пробы. */
static unsigned int enter(void *u,pcan_boot_byte_t session)
{
(void)u;
if(!healthy) return 2;
if(F28335_BOOT_LOADER) return 0;
/* No destructive fallback on boards still starting through the SPI ROM. */
if(F28335Boot_RunningSlot()>1 || !confirmed) return 2;
reset_session=session; reset_requested=1; return 1;
}
static void info32(pcan_boot_byte_t *p,uint32_t v)
{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(i*8))&255U); }
/* Паспорт помещается в classic CAN по страницам из восьми октетов.
* NAME возвращает фрагменты строки с завершающим NUL; неизвестная страница
* отклоняется. MEMORY: 0 — ёмкость/страница/режим/running, 1 — базы A/B,
* 2 — active/pending/attempted/healthy/тайм-аут, 3 — область параметров. */
static bool info(void *u,pcan_boot_byte_t command,pcan_boot_byte_t page,pcan_boot_byte_t *out)
{
static const char name[]=F28335_CAN_BOOT_DEVICE_NAME;
unsigned int i,index; (void)u;
if(command==PCAN_BOOT_CMD_CAPABILITIES && page==0) {
/* bits: ping, discovery, identity, name, memory, A/B, confirm, rollback,
* separate loader, session timeout, readback, SCI8 validation. */
info32(out,(F28335_BOOT_LOADER || F28335Boot_RunningSlot()<2)?0xFFFUL:0x1FUL);
info32(out+4,F28335_AB_SLOT_BYTES); return true;
}
if(command==PCAN_BOOT_CMD_DEVICE_NAME) {
if((unsigned int)page*8U>=sizeof(name)) return false;
for(i=0;i<8;++i) { index=(unsigned int)page*8U+i;
out[i]=index<sizeof(name)?(pcan_boot_byte_t)name[index]:0; }
return true;
}
if(command!=PCAN_BOOT_CMD_MEMORY_INFO) return false;
if(page==0) { info32(out,0x40000UL); out[4]=0; out[5]=1;
out[6]=F28335_BOOT_LOADER?1:0; out[7]=F28335Boot_RunningSlot(); return true; }
if(page==1) { info32(out,F28335_AB_SLOT_A); info32(out+4,F28335_AB_SLOT_B); return true; }
if(page==2) { out[0]=store.active; out[1]=store.pending; out[2]=store.attempted;
out[3]=healthy?1:0; info32(out+4,boot.config.session_timeout_ms); return true; }
if(page==3) { info32(out,F28335_AB_PARAMS); info32(out+4,258); return true; }
return false;
}
/* Вызывается до разрешения прикладных IRQ. Нельзя полагаться на случайное
* содержимое RAM или оставшееся состояние confirmed от предыдущей программы. */
void F28335CanBoot_EarlyInit(void) { confirmed=0; ready=0; }
/* Общий Init для приложения и загрузчика: открываем журнал, привязываем
* callbacks, задаём идентификацию, ёмкость слота, окно 16 и тайм-аут 30 секунд.
* В загрузчике FW берётся из active manifest; приложение сообщает версию сборки. */
void F28335CanBoot_Init(void)
{
pcan_boot_config_t cfg; pcan_boot_port_t port;
f28335_boot_io_t io; unsigned int running=F28335Boot_RunningSlot();
memset(&cfg,0,sizeof(cfg)); memset(&port,0,sizeof(port));
io.read=F28335Boot_Read; io.write=F28335Boot_Write; io.user=0;
healthy=f28335_boot_open(&store,&io);
cfg.device_type=F28335_CAN_BOOT_DEVICE_TYPE; cfg.device_id=F28335_CAN_BOOT_DEVICE_ID;
cfg.product_type=F28335_CAN_BOOT_PRODUCT; cfg.hardware_revision=1;
#if F28335_BOOT_LOADER
cfg.firmware_version=store.active<2?store.image[store.active].firmware_version:0;
#else
cfg.firmware_version=F28335_CAN_BOOT_FIRMWARE_VERSION;
#endif
head=tail=overflow=reset_requested=reset_session=ready=main_passes=0; tx_busy=tx_canceling=0;
confirmed=healthy && running<2 && store.active==running && store.pending==F28335_AB_NONE;
cfg.active_slot=store.active;
cfg.slot_count=2; cfg.max_image_size=F28335_AB_SLOT_BYTES; cfg.ack_window=16;
cfg.session_timeout_ms=30000UL;
port.send=transmit; port.erase_slot=erase; port.write_slot=write_block;
port.verify_status=verify; port.set_pending_slot=commit; port.reboot=reboot;
port.confirm_running_slot=confirm; port.enter_boot=enter;
port.can_reboot=can_reboot; port.service_info=info;
if(!pcan_boot_init(&boot,&cfg,&port,0)) return;
EALLOW;
/* Меняем только MBOX3/4/6: маска 88=8+16+64. MBOX0..2 — старый обмен,
* MBOX5 — heartbeat. Приём broadcast выделен в отдельный точный mailbox,
* чтобы слишком широкая маска Boot не перехватывала сообщения приложения.
* 72=8+64 — оба RX, 16 — TX. Регистры флагов очищаются записью единицы. */
ECanaRegs.CANME.all &= ~88UL;
ECanaMboxes.MBOX3.MSGID.all=0xC0000000UL|pcan_boot_make_id(0,0,cfg.device_type,cfg.device_id,0,0);
ECanaLAMRegs.LAM3.all=0x100FFFFFUL;
ECanaMboxes.MBOX3.MSGCTRL.all=8;
/* MBOX5 belongs to the application's heartbeat. MBOX6 is discovery only. */
ECanaMboxes.MBOX6.MSGID.all=0xC0000000UL|pcan_boot_make_id(0,0,7,15,13,0);
ECanaLAMRegs.LAM6.all=0x10000000UL; /* Ignore priority only. */
ECanaMboxes.MBOX6.MSGCTRL.all=0;
ECanaRegs.CANMD.all=(ECanaRegs.CANMD.all|72UL)&~16UL;
ECanaRegs.CANRMP.all=72UL;
ECanaRegs.CANMIM.all |=72UL;
ECanaRegs.CANME.all |=88UL;
EDIS;
}
/* ISR только проверяет заголовок и копирует кадр. Чужие адреса и RTR
* отбрасываются до очереди. Глобальный адрес допускается только для discovery;
* DLC и смысл команды окончательно проверит ядро в основном цикле. */
static void receive_box(volatile struct MBOX *box,uint32_t mask)
{
unsigned int next=(head+1U)&31U, i;
unsigned long lo, hi;
uint32_t id; unsigned int type;
if(!(ECanaRegs.CANRMP.all&mask)) return;
if(box->MSGCTRL.bit.RTR) { ECanaRegs.CANRMP.all=mask; return; }
id=box->MSGID.all&0x1FFFFFFFUL;
type=pcan_boot_id_msg_type(id);
if(!(box->MSGID.all&0x80000000UL) || pcan_boot_id_route(id)!=0 ||
(type!=9 && type!=10 && type!=11 && type!=13) ||
!((pcan_boot_id_device_type(id)==F28335_CAN_BOOT_DEVICE_TYPE &&
pcan_boot_id_device_id(id)==F28335_CAN_BOOT_DEVICE_ID) ||
(type==13 && pcan_boot_id_device_type(id)==7 && pcan_boot_id_device_id(id)==15 &&
pcan_boot_id_msg_body(id)==0))) { ECanaRegs.CANRMP.all=mask; return; }
if(next==tail) overflow=1;
else {
queue[head].id=box->MSGID.all&0x1FFFFFFFUL;
queue[head].dlc=box->MSGCTRL.bit.DLC;
lo=box->MDL.all; hi=box->MDH.all;
for(i=0;i<4;++i) { queue[head].data[i]=(lo>>(8U*i))&255U;
queue[head].data[i+4]=(hi>>(8U*i))&255U; }
head=next;
}
ECanaRegs.CANRMP.all=mask;
}
void F28335CanBoot_RxISR(void)
{
receive_box(&ECanaMboxes.MBOX3,8UL);
receive_box(&ECanaMboxes.MBOX6,64UL);
}
/* Вызывать после инициализации параметров, датчиков и прикладного режима.
* От этого момента отсчитывается интервал наблюдения перед авто-CONFIRM. */
void F28335CanBoot_Ready(void)
{ ready=1; ready_at=F28335Boot_Now(); }
/* Используется таймерными ISR приложения. Во время пробы кормление watchdog
* должно доказывать выполнение main: работа одного ISR не доказывает готовность.
* Старый старт без handoff сохраняет прежнюю политику watchdog. */
unsigned int F28335CanBoot_WatchdogAllowed(void)
{ return F28335Boot_RunningSlot()>1 || confirmed; }
/* Неблокирующий диспетчер относительно ожидания CAN: вызывать в каждом проходе main.
* Сами callbacks проверки/записи EEPROM синхронные. Порядок здесь важен:
* проба приложения -> завершение TX -> reset -> poll -> ошибки RX -> один кадр. */
void F28335CanBoot_Service(void)
{
uint32_t now=F28335Boot_Now();
if(!F28335_BOOT_LOADER && ready && !confirmed && healthy && F28335Boot_RunningSlot()<2) {
/* A trial must keep executing main, not merely a timer ISR. */
/* После 100 проходов main и 5 секунд пробуем сохранить CONFIRM.
* Если до 10 секунд сохранение не удалось, main перестаёт кормить watchdog,
* что позволяет следующему старту откатиться к подтверждённому образу. */
if((uint32_t)(now-ready_at)<10000UL) ServiceDog();
if(main_passes<100) ++main_passes;
if(main_passes==100 && (uint32_t)(now-ready_at)>=5000UL) (void)confirm(0);
}
if(tx_busy) {
/* После CANTRR ждём завершения отмены или запоздавшего ACK, прежде чем
* повторно использовать mailbox. reset_requested к этому моменту уже снят,
* поэтому позднее подтверждение не должно перезапустить устройство. */
if(tx_canceling) {
if(!((ECanaRegs.CANAA.all|ECanaRegs.CANTA.all)&16UL)) return;
ECanaRegs.CANAA.all=16UL; ECanaRegs.CANTA.all=16UL;
tx_busy=tx_canceling=0;
} else if((uint32_t)(now-sent_at)>250UL) {
/* Cancel reset immediately, even if a late CAN ACK races CANTRR. */
reset_requested=0; pcan_boot_abort(&boot); tail=head;
tx_canceling=1; ECanaRegs.CANTRR.all=16UL; return;
} else if(ECanaRegs.CANTA.all&16UL) {
ECanaRegs.CANTA.all=16UL; tx_busy=0;
} else return;
}
if(reset_requested) { F28335Boot_Reset(reset_session); return; }
pcan_boot_poll(&boot,now);
if(tx_busy) return;
/* После потери кадра нельзя молча продолжать запись: сбрасываем RAM-сессию
* и накопленную очередь. Подтверждённый образ и опубликованный pending сохраняются. */
if(overflow || (ECanaRegs.CANRML.all&72UL)) {
overflow=0; ECanaRegs.CANRML.all=72UL;
pcan_boot_abort(&boot); tail=head; return;
}
if(tail!=head) {
pcan_boot_process(&boot,queue[tail].id,queue[tail].data,queue[tail].dlc);
tail=(tail+1U)&31U;
}
}
/* Используем session из NOINIT только для нового ENTER_BOOT после reset.
* Это отправка отложенного ACK готовности, а не восстановление принятых блоков
* или CRC незавершённой передачи: данные сессии через reset не сохраняются. */
void F28335CanBoot_Resume(unsigned int session)
{
if(F28335_BOOT_LOADER && session && session<256)
(void)pcan_boot_process(&boot,pcan_boot_make_id(1,0,
F28335_CAN_BOOT_DEVICE_TYPE,F28335_CAN_BOOT_DEVICE_ID,9,(session<<8)|2),0,0);
}
#else
void F28335CanBoot_EarlyInit(void) {}
void F28335CanBoot_Init(void) {}
void F28335CanBoot_RxISR(void) {}
void F28335CanBoot_Service(void) {}
void F28335CanBoot_Ready(void) {}
unsigned int F28335CanBoot_WatchdogAllowed(void) { return 1; }
void F28335CanBoot_Resume(unsigned int session) { (void)session; }
#endif

View File

@@ -1,28 +0,0 @@
#ifndef F28335_CAN_BOOT_H
#define F28335_CAN_BOOT_H
#include "../../include/pcan_boot.h"
/* Приложение задаёт версию из firmware_version.h до подключения этого заголовка.
* Формат поля FW: major[31:24], minor[23:16], patch[15:0]. Ноль — запасное
* значение при отсутствии версии сборки, а не прошитая здесь версия устройства. */
#ifndef F28335_CAN_BOOT_FIRMWARE_VERSION
#define F28335_CAN_BOOT_FIRMWARE_VERSION 0UL /* Board supplies the build identity. */
#endif
/* Адрес ProtoCAN состоит из Device Type (3 бита) и Device ID (4 бита).
* ProductType — отдельный код совместимости прошивки; это не CAN ID и не RS-адрес. */
#define F28335_CAN_BOOT_DEVICE_TYPE 3U
#define F28335_CAN_BOOT_DEVICE_ID 7U
#define F28335_CAN_BOOT_PRODUCT 0x2833U
#define F28335_CAN_BOOT_DEVICE_NAME "F28335-M95M02"
/* Порядок интеграции приложения:
* StartupWatchdog/EarlyInit сразу после InitSysCtrl; Init после настройки
* таймера и EEPROM; Ready после старта прикладных подсистем; Service в main.
* RxISR вызывается из CAN ISR, WatchdogAllowed проверяется перед кормлением
* watchdog таймерным ISR. Resume применяется только отдельным загрузчиком. */
void F28335CanBoot_Init(void);
void F28335CanBoot_EarlyInit(void);
void F28335CanBoot_RxISR(void);
void F28335CanBoot_Service(void);
void F28335CanBoot_Ready(void);
void F28335CanBoot_Resume(unsigned int session);
unsigned int F28335CanBoot_WatchdogAllowed(void);
#endif

View File

@@ -1,16 +1,21 @@
/* ProtoCAN update state machine; platform owns storage and reset. */
#include "../include/pcan_boot.h"
/*
* Автомат адресной загрузки прошивки по ProtoCAN с сессией, контрольной суммой и A/B-слотами.
* Приём блоков, проверка образа и активация разделены на этапы; операции Flash и метаданные
* слотов предоставляет платформа.
*/
#include "pcan_boot.h"
#include <string.h>
#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL
static uint16_t get_u16(const pcan_boot_byte_t *p)
static uint16_t get_u16(const uint8_t *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t get_u32(const pcan_boot_byte_t *p)
static uint32_t get_u32(const uint8_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
@@ -18,26 +23,26 @@ static uint32_t get_u32(const pcan_boot_byte_t *p)
| ((uint32_t)p[3] << 24);
}
static void put_u16(pcan_boot_byte_t *p, uint16_t value)
static void put_u16(uint8_t *p, uint16_t value)
{
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
}
static void put_u32(pcan_boot_byte_t *p, uint32_t value)
static void put_u32(uint8_t *p, uint32_t value)
{
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
p[2] = (pcan_boot_byte_t)((value >> 16) & 0xFFU);
p[3] = (pcan_boot_byte_t)(value >> 24);
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
}
static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t length)
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
{
size_t i;
crc = ~crc;
for (i = 0U; i < length; ++i) {
pcan_boot_byte_t bit;
uint8_t bit;
crc ^= data[i];
for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U);
@@ -46,9 +51,9 @@ static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t
return ~crc;
}
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
pcan_boot_byte_t msg_type, uint16_t msg_body)
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body)
{
return ((((uint32_t)priority & 1U) << 28)
| (((uint32_t)route & 1U) << 27)
@@ -58,14 +63,14 @@ uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
| msg_body) & PCAN_BOOT_ID_MASK;
}
pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 27) & 1U); }
pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 24) & 7U); }
pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 20) & 15U); }
pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 16) & 15U); }
uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); }
uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); }
uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); }
uint8_t pcan_boot_id_msg_type(uint32_t can_id) { return (uint8_t)((can_id >> 16) & 15U); }
uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; }
static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t body,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
const uint8_t *data, uint8_t dlc)
{
uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE,
boot->config.device_type,
@@ -74,12 +79,9 @@ static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t bo
return boot->port.send(boot->port_user, id, data, dlc);
}
/* STATUS всегда содержит состояние передачи: status, target_slot, next_block, CRC32.
* CRC здесь относится к уже принятому потоку, а не заменяет проверку EEPROM.
* Номер следующего блока помогает клиенту продолжить обмен после потери ACK. */
static bool send_status(pcan_boot_t *boot, pcan_boot_byte_t command, pcan_boot_byte_t status)
static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
{
pcan_boot_byte_t data[8];
uint8_t data[8];
uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command);
data[0] = status;
data[1] = boot->target_slot;
@@ -89,11 +91,9 @@ static bool send_status(pcan_boot_t *boot, pcan_boot_byte_t command, pcan_boot_b
return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U);
}
/* IDENTIFY имеет отдельный тип сообщения DISCOVERY и Body=1. Он доступен
* без открытия сессии, поэтому обнаружение устройства не должно запускать обновление. */
static bool send_identity(pcan_boot_t *boot)
{
pcan_boot_byte_t data[8];
uint8_t data[8];
put_u16(&data[0], boot->config.product_type);
data[2] = boot->config.hardware_revision;
data[3] = PCAN_BOOT_VERSION;
@@ -101,9 +101,6 @@ static bool send_identity(pcan_boot_t *boot)
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U);
}
/* Отменяется только состояние протокола в RAM: manifest, счётчики и session.
* Функция не стирает EEPROM и не снимает ранее опубликованный pending.
* Ответственность за постоянное состояние слотов остаётся у платформы. */
void pcan_boot_abort(pcan_boot_t *boot)
{
if (boot == NULL) {
@@ -121,24 +118,19 @@ void pcan_boot_abort(pcan_boot_t *boot)
boot->have_compat = false;
}
/* Конфигурация копируется в экземпляр; указатели callbacks остаются привязаны
* к port_user. Значения slot_count=0 и max_image_size=0 сохраняют исторические
* настройки по умолчанию. Для пустого A/B-хранилища допустим active_slot=NONE. */
bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
const pcan_boot_port_t *port, void *port_user)
{
pcan_boot_byte_t slot_count;
uint8_t slot_count;
if ((boot == NULL) || (config == NULL) || (port == NULL)
|| (config->device_type > 7U) || (config->device_id > 15U)
|| ((config->active_slot > 1U) && (config->active_slot != PCAN_BOOT_SLOT_NONE))
|| (config->active_slot > 1U)
|| (port->send == NULL) || (port->erase_slot == NULL)
|| (port->write_slot == NULL)) {
return false;
}
slot_count = config->slot_count == 0U ? 2U : config->slot_count;
if ((slot_count > 2U) || ((config->active_slot >= slot_count) &&
(config->active_slot != PCAN_BOOT_SLOT_NONE || slot_count != 2U))
|| config->max_image_size > PCAN_BOOT_SLOT_SIZE
if ((slot_count > 2U) || (config->active_slot >= slot_count)
|| ((slot_count == 2U) && (port->set_pending_slot == NULL))) {
return false;
}
@@ -157,10 +149,7 @@ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
return true;
}
/* BEGIN_IMAGE и BEGIN_COMPAT приходят отдельными кадрами. До получения обоих
* не разрешаем ERASE. Проверяем размер, Product, HW и необязательную политику
* authorize. В A/B выбираем противоположный active слот; для первого образа — A. */
static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
{
if (!boot->have_image || !boot->have_compat) {
boot->state = PCAN_BOOT_STATE_METADATA;
@@ -171,11 +160,8 @@ static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if (boot->manifest.product_type != boot->config.product_type) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_DEVICE);
}
if ((boot->config.hardware_revision < boot->manifest.hardware_revision_min)
if ((boot->manifest.product_type != boot->config.product_type)
|| (boot->config.hardware_revision < boot->manifest.hardware_revision_min)
|| (boot->config.hardware_revision > boot->manifest.hardware_revision_max)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE);
@@ -185,51 +171,19 @@ static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR);
}
boot->target_slot = boot->config.active_slot == PCAN_BOOT_SLOT_NONE ? 0U :
boot->config.slot_count == 1U
boot->target_slot = boot->config.slot_count == 1U
? boot->config.active_slot
: (pcan_boot_byte_t)(boot->config.active_slot ^ 1U);
: (uint8_t)(boot->config.active_slot ^ 1U);
boot->state = PCAN_BOOT_STATE_READY_TO_ERASE;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
/* Body состоит из session в старшем октете и command в младшем.
* Сначала проверяем DLC, чтобы обработчики не читали отсутствующие данные.
* Идентификация и паспорт не открывают сессию; CONFIRM обрабатывается отдельно,
* поскольку приложение не должно входить в загрузчик ради подтверждения себя.
* Остальные команды изменяют RAM-автомат только в допустимой сессии/состоянии. */
static bool process_control(pcan_boot_t *boot, uint16_t body,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
const uint8_t *data, uint8_t dlc)
{
pcan_boot_byte_t session = (pcan_boot_byte_t)(body >> 8);
pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU);
uint8_t session = (uint8_t)(body >> 8);
uint8_t command = (uint8_t)body;
if (dlc != ((command == PCAN_BOOT_CMD_BEGIN_IMAGE || command == PCAN_BOOT_CMD_BEGIN_COMPAT) ? 8U :
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO) ? 1U : 0U)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if (command == PCAN_BOOT_CMD_PING) {
pcan_boot_byte_t reply[8] = {0};
reply[0] = PCAN_BOOT_VERSION; reply[1] = boot->state;
reply[2] = boot->config.active_slot; reply[3] = boot->target_slot;
reply[4] = boot->session_id; reply[5] = boot->config.slot_count;
reply[6] = boot->last_status;
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, command, reply, 8);
}
if (command >= PCAN_BOOT_CMD_CAPABILITIES && command <= PCAN_BOOT_CMD_MEMORY_INFO) {
pcan_boot_byte_t reply[8] = {0};
pcan_boot_byte_t page = dlc ? data[0] : 0;
if (!boot->port.service_info || !boot->port.service_info(boot->port_user,command,page,reply))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_COMMAND);
return send_frame(boot,PCAN_BOOT_MSG_DISCOVERY,(uint16_t)((page<<8)|command),reply,8);
}
if (command == PCAN_BOOT_CMD_CONFIRM) {
pcan_boot_byte_t saved=boot->session_id; bool sent;
boot->session_id=session;
sent=send_status(boot,command,session && boot->port.confirm_running_slot &&
boot->port.confirm_running_slot(boot->port_user) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_INVALID_STATE);
boot->session_id=saved; return sent;
}
if (command == PCAN_BOOT_CMD_IDENTIFY) {
return send_identity(boot);
}
@@ -237,18 +191,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
if (session == 0U) {
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
if (boot->state != PCAN_BOOT_STATE_IDLE && boot->session_id != session)
return send_status(boot,command,PCAN_BOOT_STATUS_BUSY);
if (boot->port.enter_boot) {
unsigned int result=boot->port.enter_boot(boot->port_user,session);
if(result==1) return true;
if(result!=0) return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
}
if (boot->session_id==session && boot->state!=PCAN_BOOT_STATE_IDLE)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
pcan_boot_abort(boot);
boot->session_id = session;
boot->last_activity_ms=boot->now_ms;
boot->state = PCAN_BOOT_STATE_METADATA;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
@@ -256,11 +200,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
boot->last_activity_ms=boot->now_ms;
switch (command) {
case PCAN_BOOT_CMD_BEGIN_IMAGE:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.image_size==get_u32(data) &&
boot->manifest.image_crc32==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -270,9 +211,6 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_BEGIN_COMPAT:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.product_type==get_u16(data) &&
boot->manifest.hardware_revision_min==data[2] && boot->manifest.hardware_revision_max==data[3] &&
boot->manifest.firmware_version==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -284,8 +222,6 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_ERASE:
if(boot->state==PCAN_BOOT_STATE_RECEIVING && boot->bytes_received==0)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -300,8 +236,6 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_VERIFY:
if(boot->state==PCAN_BOOT_STATE_VERIFIED || boot->state==PCAN_BOOT_STATE_COMMITTED)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((boot->state != PCAN_BOOT_STATE_RECEIVING)
|| (boot->bytes_received != boot->manifest.image_size)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
@@ -310,13 +244,7 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR);
}
if (boot->port.verify_status) {
pcan_boot_status_t status=boot->port.verify_status(boot->port_user,boot->target_slot,&boot->manifest);
if(status!=PCAN_BOOT_STATUS_OK) {
boot->state=PCAN_BOOT_STATE_FAILED; return send_status(boot,command,status);
}
}
if (!boot->port.verify_status && (boot->port.verify_image != NULL)
if ((boot->port.verify_image != NULL)
&& !boot->port.verify_image(boot->port_user, boot->target_slot,
&boot->manifest)) {
boot->state = PCAN_BOOT_STATE_FAILED;
@@ -326,16 +254,21 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_COMMIT:
if(boot->state==PCAN_BOOT_STATE_COMMITTED) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_VERIFIED) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
if ((boot->port.set_pending_slot != NULL)
if ((boot->config.slot_count == 2U)
&& !boot->port.set_pending_slot(boot->port_user, boot->target_slot,
&boot->manifest)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
boot->state=PCAN_BOOT_STATE_COMMITTED;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_CONFIRM:
if ((boot->port.confirm_running_slot == NULL)
|| !boot->port.confirm_running_slot(boot->port_user)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_QUERY_PROGRESS:
@@ -347,9 +280,7 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_REBOOT:
if(!boot->port.reboot || (boot->port.can_reboot && !boot->port.can_reboot(boot->port_user)))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
if(!send_status(boot,command,PCAN_BOOT_STATUS_OK)) return false;
(void)send_status(boot, command, PCAN_BOOT_STATUS_OK);
if (boot->port.reboot != NULL) {
boot->port.reboot(boot->port_user);
}
@@ -360,16 +291,11 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
}
}
/* DATA не содержит session в Body: все 16 бит заняты номером блока. Поэтому
* кадры данных принимаются только в состоянии RECEIVING и для выбранного A/B.
* При неверном номере ничего не записываем и возвращаем ожидаемый next_block.
* Последний кадр дополняется клиентом до 8 октетов, но запись и CRC учитывают
* только оставшиеся октеты image_size. ACK выдаётся на границе окна или в конце. */
static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t block,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
const uint8_t *data, uint8_t dlc)
{
pcan_boot_byte_t expected_type;
pcan_boot_byte_t write_length;
uint8_t expected_type;
uint8_t write_length;
uint32_t remaining;
if (boot->state != PCAN_BOOT_STATE_RECEIVING) {
@@ -383,9 +309,8 @@ static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t
if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR);
}
boot->last_activity_ms=boot->now_ms;
remaining = boot->manifest.image_size - boot->bytes_received;
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (pcan_boot_byte_t)remaining
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (uint8_t)remaining
: PCAN_BOOT_BLOCK_SIZE;
if ((write_length == 0U)
|| !boot->port.write_slot(boot->port_user, boot->target_slot,
@@ -404,14 +329,10 @@ static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t
return true;
}
/* Общая точка входа для одного кадра после аппаратной фильтрации.
* Проверки адреса повторяются программно, чтобы широкая маска CAN не разрешила
* обновление чужого устройства. Broadcast допускается только для DISCOVERY
* с Body=0/DLC=0; ответ откладывается до poll для разведения устройств по времени. */
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
const uint8_t *data, uint8_t dlc)
{
pcan_boot_byte_t msg_type;
uint8_t msg_type;
if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) {
return false;
}
@@ -419,17 +340,6 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) {
return false;
}
if(msg_type==PCAN_BOOT_MSG_DISCOVERY && pcan_boot_id_route(can_id)==0 &&
pcan_boot_id_msg_body(can_id)==0 && dlc==0 &&
((pcan_boot_id_device_type(can_id)==7 && pcan_boot_id_device_id(can_id)==15) ||
(pcan_boot_id_device_type(can_id)==boot->config.device_type &&
pcan_boot_id_device_id(can_id)==boot->config.device_id))) {
if(!boot->discovery_pending) {
boot->discovery_at=boot->now_ms+2U*(1U+16U*boot->config.device_type+boot->config.device_id);
boot->discovery_pending=true;
}
return true;
}
if ((pcan_boot_id_route(can_id) != PCAN_BOOT_ROUTE_FROM_PM)
|| (pcan_boot_id_device_type(can_id) != boot->config.device_type)
|| (pcan_boot_id_device_id(can_id) != boot->config.device_id)) {
@@ -443,18 +353,3 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
}
return false;
}
/* Обслуживание времени не требует входящего кадра. Регулярно вызывать из main
* с монотонным счётчиком миллисекунд: иначе не истечёт сессия и не выйдет discovery.
* При занятом TX discovery остаётся pending до успешного send_identity.
* Вычитание unsigned учитывает переход счётчика через 0xFFFFFFFF. */
void pcan_boot_poll(pcan_boot_t *boot,uint32_t now_ms)
{
if(!boot) return;
boot->now_ms=now_ms;
if(boot->config.session_timeout_ms && boot->state!=PCAN_BOOT_STATE_IDLE &&
(uint32_t)(now_ms-boot->last_activity_ms)>=boot->config.session_timeout_ms)
pcan_boot_abort(boot);
if(boot->discovery_pending && (uint32_t)(now_ms-boot->discovery_at)<0x80000000UL && send_identity(boot))
boot->discovery_pending=false;
}

View File

@@ -1,13 +0,0 @@
#ifndef TEST_DSP_H
#define TEST_DSP_H
#include <stdint.h>
#define EALLOW
#define EDIS
struct test_reg { uint32_t all; struct { unsigned DLC, RTR; } bit; };
struct MBOX { struct test_reg MSGID, MSGCTRL, MDL, MDH; };
static struct { struct MBOX MBOX3, MBOX4, MBOX6; } ECanaMboxes;
static struct { struct test_reg CANME,CANMD,CANRMP,CANRML,CANMIM,CANTA,CANAA,CANTRS,CANTRR; } ECanaRegs;
static struct { struct test_reg LAM3,LAM6; } ECanaLAMRegs;
static struct { unsigned long InterruptCount; } CpuTimer1;
static struct { unsigned WDCR; } SysCtrlRegs;
#endif

View File

@@ -1,7 +0,0 @@
#include <stdint.h>
#ifndef EEPROM_M95M02
#define EEPROM_M95M02 1
#endif
#define BALSAM167_EEPROM_FIRMWARE_BYTES 261884UL
void Seeprom_write(unsigned long word, unsigned int *data, unsigned long bytes);
void Seeprom_read(unsigned long word, unsigned int *data, unsigned long bytes);

View File

@@ -1,27 +0,0 @@
"""Host regression suite. CC may point to clang/gcc; this does not replace a TI build."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
cc = os.environ.get('CC') or shutil.which('clang') or shutil.which('gcc')
if not cc:
raise SystemExit('Set CC to a C99 host compiler')
store = 'ports/tms320f28335/f28335_boot_store.c'
cases = [
('core', [], ['src/pcan_boot.c', 'tests/test_pcan_boot.c']),
('store_native', [], [store, 'tests/test_f28335_boot_store.c']),
('store_wide', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'tests/test_f28335_boot_store.c']),
('loader', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']),
('application', ['-DPCAN_BOOT_WIDE_BYTES', '-DF28335_BOOT_LOADER=0'],
[store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']),
]
with tempfile.TemporaryDirectory(prefix='f28335-ab-') as tmp:
for name, defines, sources in cases:
exe = Path(tmp) / (name + '.exe')
subprocess.run([cc, '-std=c99', '-Wall', '-Wextra', '-Werror', '-Iinclude',
'-Itests/f28335_stubs', *defines, *sources, '-o', str(exe)], cwd=root, check=True)
sample = os.environ.get('F28335_SAMPLE_IMAGE') if name == 'store_wide' else None
subprocess.run([str(exe), *([sample] if sample else [])], check=True, timeout=30)

View File

@@ -1,118 +0,0 @@
#define _CRT_SECURE_NO_WARNINGS
#include "../ports/tms320f28335/f28335_boot_store.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static unsigned char eeprom[0x40000],backup[0x40000];
static long budget=-1;
static int read_fail;
static unsigned written_words;
static bool read_mem(void *u,uint32_t addr,pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(addr+n<=sizeof(eeprom)); if(read_fail) return false;
for(i=0;i<n;++i) p[i]=eeprom[addr+i]; return true; }
static bool write_mem(void *u,uint32_t addr,const pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(addr+n<=0x3FE00);
for(i=0;i<n;++i) { if(budget==0) return false; if(budget>0) --budget; eeprom[addr+i]=(unsigned char)p[i]; }
return true; }
static f28335_boot_io_t io={read_mem,write_mem,0};
static pcan_boot_byte_t image[40];
static pcan_boot_manifest_t manifest;
static void make_image(void)
{
memset(image,0,sizeof(image)); image[0]=0xAA; image[1]=8;
image[20]=0; image[21]=0x80; /* entry=8000, high-word first */
image[22]=5; image[26]=0; image[27]=0x80;
image[28]=0x34; image[29]=0x12;
memset(&manifest,0,sizeof(manifest)); manifest.product_type=0x2833;
manifest.hardware_revision_min=manifest.hardware_revision_max=1;
manifest.image_size=40; manifest.image_crc32=f28335_boot_crc(0,image,40);
manifest.firmware_version=0x01010002;
}
static void image_to(f28335_boot_store_t *s,unsigned slot)
{ unsigned i; assert(f28335_boot_prepare(s,slot));
for(i=0;i<40;i+=8) assert(f28335_boot_write(s,slot,i,image+i,8)); }
static bool ram(void *u,uint32_t address,uint16_t word)
{ (void)u; assert(address==0x8000+written_words); if(!written_words) assert(word==0x1234);
++written_words; return true; }
static void reopen(f28335_boot_store_t *s) { assert(f28335_boot_open(s,&io)); }
int main(int argc,char **argv)
{
f28335_boot_store_t s; unsigned slot; uint32_t entry; long cut;
memset(eeprom,0xA5,sizeof(eeprom)); make_image(); reopen(&s);
assert(s.active==255 && !s.valid); assert(f28335_boot_select(&s,&slot) && slot==255);
image_to(&s,0); assert(f28335_boot_verify(&s,0,&manifest,&entry) && entry==0x8000);
assert(f28335_boot_commit(&s,0,&manifest));
assert(!f28335_boot_confirm(&s,0)); /* Cannot confirm an image not yet started. */
reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.attempted);
assert(f28335_boot_load(&s,slot,ram,0,&entry) && written_words==5);
assert(!f28335_boot_confirm(&s,1)); assert(f28335_boot_confirm(&s,0));
assert(f28335_boot_confirm(&s,0)); /* idempotent */
assert(!f28335_boot_prepare(&s,0));
assert(!f28335_boot_write(&s,0,0,image,8));
assert(!f28335_boot_write(&s,1,F28335_AB_SLOT_BYTES,image,8));
image_to(&s,1); memcpy(backup,eeprom,sizeof(eeprom));
/* Every torn write position in journal publication keeps confirmed A. */
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
(void)f28335_boot_commit(&s,1,&manifest); budget=-1; reopen(&s);
assert(s.active==0); assert(f28335_boot_verify(&s,0,&s.image[0],0));
assert(s.pending==255 || s.pending==1);
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_commit(&s,1,&manifest));
memcpy(backup,eeprom,sizeof(eeprom));
/* Torn trial bookkeeping must never select an unrecorded trial. */
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
if(f28335_boot_select(&s,&slot)) { assert(slot==1); assert(s.attempted); }
budget=-1; reopen(&s); assert(s.active==0);
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s);
assert(f28335_boot_select(&s,&slot) && slot==1);
reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.pending==255);
image_to(&s,1); assert(f28335_boot_commit(&s,1,&manifest));
assert(f28335_boot_select(&s,&slot) && slot==1);
memcpy(backup,eeprom,sizeof(eeprom));
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
(void)f28335_boot_confirm(&s,1); budget=-1; reopen(&s);
assert(s.active==0 || s.active==1);
assert(f28335_boot_select(&s,&slot) && (slot==0 || slot==1));
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_confirm(&s,1));
/* CRC correct but malicious SCI8 destination and entry are rejected. */
image_to(&s,0); image[26]=0; image[27]=0xE0;
manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0));
image[27]=0x80; image[21]=0x90; manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0));
make_image(); image[22]=1; image[30]=1; image[35]=0x80;
manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* overlapping sections */
make_image(); image_to(&s,0); manifest.image_size=38;
manifest.image_crc32=f28335_boot_crc(0,image,38);
assert(!f28335_boot_verify(&s,0,&manifest,0)); /* missing terminator */
make_image(); image[22]=4; manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* trailing octets */
make_image(); image_to(&s,0); eeprom[F28335_AB_SLOT_A+29]^=1;
assert(!f28335_boot_verify(&s,0,&manifest,0));
/* Failed EEPROM reads fail closed, including subsequent writes. */
read_fail=1; assert(!f28335_boot_open(&s,&io));
assert(!f28335_boot_prepare(&s,0)); assert(!f28335_boot_write(&s,0,0,image,8)); read_fail=0;
for(entry=0x3FE00;entry<0x40000;++entry) assert(eeprom[entry]==0xA5);
if(argc>1) {
FILE *file=fopen(argv[1],"rb"); uint32_t offset,crc=0;
pcan_boot_byte_t bytes[32]; unsigned n;
assert(file); make_image();
manifest.image_size=(uint32_t)fread(eeprom+F28335_AB_SLOT_A,1,F28335_AB_SLOT_BYTES,file);
assert(!ferror(file) && fgetc(file)==EOF); fclose(file); reopen(&s);
for(offset=0;offset<manifest.image_size;offset+=n) {
n=manifest.image_size-offset<32?(unsigned)(manifest.image_size-offset):32;
assert(read_mem(0,F28335_AB_SLOT_A+offset,bytes,n)); crc=f28335_boot_crc(crc,bytes,n);
}
manifest.image_crc32=crc;
assert(f28335_boot_verify(&s,0,&manifest,&entry));
puts("Production SCI8 binary: CRC, destinations and entry accepted");
}
puts("F28335 A/B: journal power cuts, trial, rollback, confirm, bounds, SCI8: OK");
return 0;
}

View File

@@ -1,94 +0,0 @@
/* Production CAN adapter, fake hardware, wide C28x wire octets. */
#define EEPROM_M95M02 1
#ifndef F28335_BOOT_LOADER
#define F28335_BOOT_LOADER 1
#endif
#define F28335_CAN_BOOT_FIRMWARE_VERSION 0x12345678UL
#include <assert.h>
#include <stdio.h>
#include <string.h>
void ServiceDog(void);
#include "../ports/tms320f28335/f28335_can_boot.c"
static unsigned char memory[0x40000];
static unsigned int running=255,resets,reset_arg,dog_feeds;
static int fail_write;
void ServiceDog(void) { ++dog_feeds; }
bool F28335Boot_Read(void *u,uint32_t a,pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(a+n<=sizeof(memory)); for(i=0;i<n;++i)p[i]=memory[a+i]; return true; }
bool F28335Boot_Write(void *u,uint32_t a,const pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(a+n<=0x3FE00); if(fail_write)return false;
for(i=0;i<n;++i)memory[a+i]=(unsigned char)p[i]; return true; }
uint32_t F28335Boot_Now(void) { return CpuTimer1.InterruptCount; }
unsigned int F28335Boot_RunningSlot(void) { return running; }
void F28335Boot_Reset(unsigned int session) { ++resets;reset_arg=session;reset_requested=0; }
static void command(unsigned cmd,const pcan_boot_byte_t *d,unsigned n)
{ tx_busy=0; assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,3,7,9,0x100|cmd),d,n)); }
static void put32(pcan_boot_byte_t *p,uint32_t v)
{ unsigned i;for(i=0;i<4;++i)p[i]=(v>>(i*8))&255; }
int main(void)
{
pcan_boot_byte_t image[40]={0xAA,8},meta[8],compat[8]={0x33,0x28,1,1,2,0,1,1};
unsigned i,slot; pcan_boot_manifest_t m;
memset(memory,0xA5,sizeof(memory)); image[21]=0x80; image[22]=5; image[27]=0x80;
F28335CanBoot_Init(); assert(boot.config.max_image_size==130560 && boot.config.slot_count==2);
if(!F28335_BOOT_LOADER) {
/* SPI-ROM application without Flash-loader handoff cannot erase anything. */
command(2,0,0); assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE && !reset_requested);
memset(&m,0,sizeof(m));m.image_size=40;m.image_crc32=f28335_boot_crc(0,image,40);
m.product_type=0x2833;m.hardware_revision_min=m.hardware_revision_max=1;
assert(f28335_boot_prepare(&store,0));
for(i=0;i<40;i+=8)assert(f28335_boot_write(&store,0,i,image+i,8));
assert(f28335_boot_commit(&store,0,&m)); assert(f28335_boot_select(&store,&slot));
running=0; F28335CanBoot_Init();
assert(!F28335CanBoot_WatchdogAllowed());
command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE);
F28335CanBoot_Ready();
for(i=0;i<100;++i) F28335CanBoot_Service();
assert(!confirmed && store.pending==0);
CpuTimer1.InterruptCount=5000; F28335CanBoot_Service();
assert(confirmed && store.active==0 && dog_feeds);
command(2,0,0); assert(reset_requested && reset_session==1 && boot.state==PCAN_BOOT_STATE_IDLE);
F28335CanBoot_Service();assert(resets==1 && reset_arg==1);
command(1,0,0);assert(ECanaMboxes.MBOX4.MDH.all==0x12345678UL);
} else {
command(2,0,0); put32(meta,40);put32(meta+4,f28335_boot_crc(0,image,40));
command(3,meta,8);command(4,compat,8);assert(boot.target_slot==0);
command(5,0,0);
for(i=0;i<5;++i) { tx_busy=0; assert(pcan_boot_process(&boot,
pcan_boot_make_id(1,0,3,7,10,i),image+i*8,8)); }
command(6,0,0);assert(boot.state==PCAN_BOOT_STATE_VERIFIED);
command(7,0,0);assert(store.pending==0 && boot.state==PCAN_BOOT_STATE_COMMITTED);
command(7,0,0);assert(boot.last_status==0);
command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE);
command(9,0,0);assert(reset_requested);tx_busy=0;
F28335CanBoot_Service();assert(resets==1 && reset_arg==0);
command(10,0,0);command(2,0,0);command(3,meta,8);command(4,compat,8);
command(5,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_FLASH_ERROR); /* Pending is protected. */
}
command(12,0,0); assert((ECanaMboxes.MBOX4.MSGID.all&65535)==12);
command(13,0,0); assert(ECanaMboxes.MBOX4.MDH.all==130560);
meta[0]=0;command(14,meta,1); assert(ECanaMboxes.MBOX4.MDL.all==0x33383246UL); /* F283 */
meta[0]=3;command(15,meta,1);assert(ECanaMboxes.MBOX4.MDL.all==0x3FEFCUL);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,3,6,9,0x102),0,0));
tx_busy=0;
assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),0,0));
assert(boot.discovery_pending); pcan_boot_poll(&boot,boot.discovery_at);
assert(!boot.discovery_pending && (ECanaMboxes.MBOX4.MSGID.all&65535)==1);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x102),0,0));
head=tail=0; ECanaRegs.CANRMP.all=8;
ECanaMboxes.MBOX3.MSGID.all=0x80000000UL|pcan_boot_make_id(1,0,3,7,9,12);
ECanaMboxes.MBOX3.MSGCTRL.bit.DLC=0;
F28335CanBoot_RxISR(); assert(head==1 && tail==0);
tx_busy=0; F28335CanBoot_Service(); assert(tail==1);
ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=1; F28335CanBoot_RxISR(); assert(head==1);
ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=0;
overflow=1;tx_busy=0;F28335CanBoot_Service();assert(boot.state==PCAN_BOOT_STATE_IDLE);
/* An ACK arriving after cancellation must never execute a queued reset. */
tx_busy=1;tx_canceling=0;reset_requested=1;sent_at=0;
CpuTimer1.InterruptCount=300;ECanaRegs.CANTA.all=0;ECanaRegs.CANAA.all=0;
F28335CanBoot_Service();assert(!reset_requested && tx_canceling);
ECanaRegs.CANTA.all=16; F28335CanBoot_Service();assert(!tx_busy && resets==1);
for(i=0x3FE00;i<0x40000;++i)assert(memory[i]==0xA5);
puts(F28335_BOOT_LOADER?"F28335 loader CAN services: OK":"F28335 application handoff/confirmation: OK");
return 0;
}

View File

@@ -264,7 +264,7 @@ static void test_rejects_wrong_session_and_compatibility(void)
data[3] = 3U;
put_u32(&data[4], 1U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U));
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_DEVICE);
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_HARDWARE);
assert(boot.state == PCAN_BOOT_STATE_FAILED);
}
@@ -306,41 +306,8 @@ static void test_single_slot_update(void)
assert(memcmp(fake.flash[0], image, sizeof(image)) == 0);
}
static void test_services_timeout_and_retries(void)
{
pcan_boot_t boot; fake_t fake;
uint8_t image[8]={0},compat[8]={0}; unsigned count;
setup(&boot,&fake); boot.config.session_timeout_ms=30000;
pcan_boot_poll(&boot,0xFFFFFF00UL);
assert(command(&boot,7,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0));
assert(command(&boot,8,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0));
assert(boot.session_id==7 && fake.last_data[0]==PCAN_BOOT_STATUS_BUSY);
assert(command(&boot,7,PCAN_BOOT_CMD_ERASE,image,1));
assert(fake.last_data[0]==PCAN_BOOT_STATUS_INVALID_SIZE);
pcan_boot_poll(&boot,29743); assert(boot.state!=PCAN_BOOT_STATE_IDLE);
pcan_boot_poll(&boot,29744); assert(boot.state==PCAN_BOOT_STATE_IDLE);
assert(command(&boot,0,PCAN_BOOT_CMD_PING,NULL,0));
assert(pcan_boot_id_msg_body(fake.last_id)==12);
count=fake.sends;
assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),NULL,0));
assert(fake.sends==count); pcan_boot_poll(&boot,boot.discovery_at);
assert(fake.sends==count+1 && pcan_boot_id_msg_body(fake.last_id)==1);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x702),NULL,0));
begin_update(&boot,image,8);
put_u16(compat,boot.manifest.product_type);
compat[2]=boot.manifest.hardware_revision_min; compat[3]=boot.manifest.hardware_revision_max;
put_u32(compat+4,boot.manifest.firmware_version);
assert(command(&boot,7,4,compat,8)); assert(fake.last_data[0]==0);
assert(command(&boot,7,5,NULL,0)); count=fake.erases;
assert(command(&boot,7,5,NULL,0)); assert(fake.erases==count && fake.last_data[0]==0);
assert(pcan_boot_process(&boot,request_id(PCAN_BOOT_MSG_DATA_B,0),image,8));
assert(command(&boot,7,6,NULL,0)); assert(command(&boot,7,6,NULL,0)); assert(fake.last_data[0]==0);
assert(command(&boot,7,7,NULL,0)); assert(command(&boot,7,7,NULL,0)); assert(fake.last_data[0]==0);
}
int main(void)
{
test_services_timeout_and_retries();
test_id_layout();
test_crc32_reference();
test_complete_update();

View File

@@ -31,8 +31,7 @@ enum set_plot_operation {
PCAN_ABI_API uint32_t set_plot_abi_version(void);
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
* TRANSFORM rejects malformed viewports; invalid gesture values return the
* unchanged viewport. Zoom is restricted to 1..10000000 on X and 1..128 on Y,
* with focus anchoring.
* unchanged viewport. Zoom is restricted to 1..128, with focus anchoring.
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
*/
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,

View File

@@ -13,7 +13,6 @@ extern "C" {
#endif
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE };
/* Caller-owned workspace: at least 14*count+128 doubles. No heap or globals.
* Bounds: 2..100000 input points, 2..1000000 output points.
* Inputs may be unsorted; duplicate times are averaged. No extrapolation.
* meta[0..2] = original count, unique count, RMSE at original measurements.
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last).

View File

@@ -0,0 +1,206 @@
package ru.setcorp.setprotocol.deif
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONArray
import org.json.JSONObject
import ru.setcorp.setflash.core.CanBridgeProtocol
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
data class DeifBinding(val index: Int, val subindex: Int = 2, val scale: Double = 1.0)
data class DeifDisplayConfig(
val node: Int = 1, val canId: Int = 0x201,
val bindings: Map<Parameter, DeifBinding> = Parameter.entries.associateWith { DeifBinding(it.index, 2, it.scale) },
)
data class DeifReading(val value: Double, val at: Long)
data class DeifState(
val config: DeifProtocol.Config = DeifProtocol.Config(),
val raw: Map<Parameter, Int> = Parameter.entries.associateWith { 0 },
val controlScales: DeifControls.Scales = DeifControls.Scales(),
val unlocked: Boolean = false, val running: Boolean = false,
val fullScreen: Boolean = false,
val displays: List<DeifDisplayConfig> = List(2) { DeifDisplayConfig() },
val readings: List<Map<Parameter, DeifReading>> = List(2) { emptyMap() },
val sent: Long = 0, val received: Long = 0, val errors: Long = 0,
val echoes: Long = 0, val echoTimes: Map<Int, Long> = emptyMap(),
val intervalMs: Long = 0, val maxGapMs: Long = 0, val maxCycleMs: Long = 0,
val status: String = "Передача выключена", val lastReceived: String = "Нет принятых данных",
)
/** Owns both timers and display freshness independently of the Android screen lifecycle. */
class DeifController(
private val scope: CoroutineScope,
private val send: suspend (CanBridgeProtocol.Frame) -> Unit,
private val save: (String) -> Unit = {},
private val now: () -> Long = { System.nanoTime() / 1_000_000 },
) {
private val mutable = MutableStateFlow(DeifState())
val state = mutable.asStateFlow()
private var job: Job? = null
private val wire = Mutex()
private var previousCycle: Long? = null
fun setFullScreen(enabled: Boolean) { mutable.value = state.value.copy(fullScreen = enabled) }
fun stop(message: String = "Передача остановлена") {
job?.cancel(); job = null
mutable.value = mutable.value.copy(running = false, status = message)
}
fun unlock(value: Boolean) {
stop()
mutable.value = mutable.value.copy(unlocked = value)
}
fun configure(config: DeifProtocol.Config) {
check(state.value.unlocked) { "Разблокируйте настройки" }
config.validate(); stop()
mutable.value = state.value.copy(config = config)
persist()
}
fun select(parameter: Parameter, enabled: Boolean) {
stop()
val c = state.value.config
mutable.value = state.value.copy(config = c.copy(enabled = if (enabled) c.enabled + parameter else c.enabled - parameter))
persist()
}
fun setRaw(parameter: Parameter, raw: Int) {
require(raw in -32768..32767)
mutable.value = state.value.copy(raw = state.value.raw + (parameter to raw))
persist()
}
fun requestInstrument(field: DeifControls.Field, value: Double) {
mutable.value = state.value.copy(raw = state.value.raw + DeifControls.raw(field, value, state.value.controlScales))
persist()
}
fun configureControls(scales: DeifControls.Scales) {
check(state.value.unlocked); scales.validate(); stop()
mutable.value = state.value.copy(controlScales = scales)
persist()
}
fun configureDisplay(side: Int, config: DeifDisplayConfig) {
check(state.value.unlocked)
require(config.node in 1..127 && config.canId in 0x201..0x57F)
require(config.bindings.values.all { it.index in 0..65535 && it.subindex in 0..255 && it.scale.isFinite() })
stop()
mutable.value = state.value.copy(
displays = state.value.displays.mapIndexed { i, d -> if (i == side) config else d },
readings = state.value.readings.mapIndexed { i, d -> if (i == side) emptyMap() else d },
)
persist()
}
fun start(repeat: Boolean) {
if (state.value.unlocked) { stop("Заблокируйте настройки перед передачей"); return }
stop()
if (!repeat && state.value.config.enabled.isEmpty()) return
if (state.value.config.enabled.isEmpty() && !state.value.config.heartbeat) return
previousCycle = null
mutable.value = state.value.copy(running = true, sent = 0, errors = 0, echoes = 0, echoTimes = emptyMap(), maxGapMs = 0, maxCycleMs = 0)
job = scope.launch {
try {
coroutineScope {
if (repeat && state.value.config.heartbeat) launch {
while (isActive) {
transmit(DeifProtocol.heartbeat(state.value.config)); delay(500)
}
}
do {
val started = now()
val snapshot = state.value
val frames = DeifProtocol.cycle(snapshot.config, snapshot.raw)
frames.forEach { transmit(it) }
val elapsed = now() - started
val gap = previousCycle?.let { started - it } ?: 0
previousCycle = started
mutable.value = state.value.copy(intervalMs = gap,
maxGapMs = maxOf(state.value.maxGapMs, gap), maxCycleMs = maxOf(state.value.maxCycleMs, elapsed))
if (!repeat) break
delay(maxOf(1, snapshot.config.periodMs - elapsed))
} while (isActive)
}
mutable.value = state.value.copy(running = false)
} catch (cancelled: CancellationException) { throw cancelled }
catch (error: Exception) { stop("Ошибка передачи: ${error.message}") }
}
}
private suspend fun transmit(frame: CanBridgeProtocol.Frame) = wire.withLock {
currentCoroutineContext().ensureActive()
check(!state.value.unlocked)
DeifProtocol.validateTransmit(frame)
send(frame)
accept(0, frame)
mutable.value = state.value.copy(sent = state.value.sent + 1, status = "Передано в транспорт; приём прибором не подтверждён")
}
fun receive(frame: CanBridgeProtocol.Frame) {
if (frame.isError) {
mutable.value = state.value.copy(errors = state.value.errors + 1, lastReceived = "Ошибка CAN: %03X".format(frame.canId))
return
}
if (frame.toCan) {
val d = DeifProtocol.decode(frame)
if (d.index != null && d.subindex != null) mutable.value = state.value.copy(
echoes = state.value.echoes + 1, echoTimes = state.value.echoTimes + ((d.index * 256 + d.subindex) to now()))
return // Transport TX notifications must never masquerade as SLAVE RX.
}
val decoded = DeifProtocol.decode(frame)
mutable.value = state.value.copy(received = state.value.received + 1, lastReceived = decoded.summary)
accept(1, frame)
}
private fun accept(side: Int, frame: CanBridgeProtocol.Frame) {
val d = DeifProtocol.decode(frame)
if (d.raw == null) return
val config = state.value.displays[side]
if (d.node != config.node || (d.mode == DeifProtocol.Mode.SAM && frame.canId != config.canId.toLong())) return
val readings = state.value.readings[side].toMutableMap()
config.bindings.forEach { (key, binding) ->
if (binding.index == d.index && binding.subindex == d.subindex) {
if (d.valid) readings[key] = DeifReading(d.raw * binding.scale, now()) else readings.remove(key)
}
}
mutable.value = state.value.copy(readings = state.value.readings.mapIndexed { i, r -> if (i == side) readings else r })
}
fun value(side: Int, parameter: Parameter): Double? = state.value.readings[side][parameter]
?.takeIf { now() - it.at <= 5000 }?.value
fun echoAge(index: Int, subindex: Int): Long? = state.value.echoTimes[index * 256 + subindex]?.let { now() - it }
fun restore(text: String) {
runCatching {
val root = JSONObject(text)
val c = DeifProtocol.Config(DeifProtocol.Mode.valueOf(root.getString("mode")), root.getInt("node"),
root.getInt("canId"), root.getLong("period"), root.getBoolean("heartbeat"), root.getInt("masterNode"),
Parameter.entries.filter { root.getJSONArray("enabled").toString().contains("\"${it.name}\"") }.toSet())
c.validate()
val raw = Parameter.entries.associateWith { root.getJSONObject("raw").getInt(it.name).also { v -> require(v in -32768..32767) } }
val displays = (0..1).map { side ->
val o = root.getJSONArray("displays").getJSONObject(side)
DeifDisplayConfig(o.getInt("node"), o.getInt("canId"), Parameter.entries.associateWith {
val b = o.getJSONObject(it.name)
DeifBinding(b.getInt("index"), b.getInt("sub"), b.getDouble("scale"))
})
}
require(displays.all { it.node in 1..127 && it.canId in 0x201..0x57F && it.bindings.values.all { b -> b.index in 0..65535 && b.subindex in 0..255 && b.scale.isFinite() } })
val controls = root.optJSONObject("controls")
val scales = controls?.let { DeifControls.Scales(it.getDouble("power100"), it.getDouble("rpm100"),
it.getDouble("angleRawDegree"), it.getDouble("powerPercentRaw")) } ?: DeifControls.Scales()
scales.validate()
mutable.value = DeifState(config = c, raw = raw, displays = displays, controlScales = scales)
}.onFailure { mutable.value = DeifState(status = "Сохранённые настройки DEIF повреждены; загружены значения по умолчанию") }
}
private fun persist() {
val s = state.value; val c = s.config
val root = JSONObject().put("mode", c.mode.name).put("node", c.node).put("canId", c.canId)
.put("controls", JSONObject().put("power100", s.controlScales.power100).put("rpm100", s.controlScales.rpm100)
.put("angleRawDegree", s.controlScales.angleRawDegree).put("powerPercentRaw", s.controlScales.powerPercentRaw))
.put("period", c.periodMs).put("heartbeat", c.heartbeat).put("masterNode", c.masterNode)
.put("enabled", JSONArray(c.enabled.map { it.name }))
.put("raw", JSONObject().also { o -> s.raw.forEach { (p, v) -> o.put(p.name, v) } })
.put("displays", JSONArray().also { a -> s.displays.forEach { d ->
a.put(JSONObject().put("node", d.node).put("canId", d.canId).also { o -> d.bindings.forEach { (p, b) ->
o.put(p.name, JSONObject().put("index", b.index).put("sub", b.subindex).put("scale", b.scale))
} })
} })
save(root.toString())
}
}

View File

@@ -0,0 +1,45 @@
package ru.setcorp.setprotocol.deif
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
import kotlin.math.*
/** SETGUI Propulsor uses the same 600 × 510 coordinate system and calibration. */
object DeifControls {
enum class Field { POWER, RPM, ANGLE }
data class Scales(val power100: Double = 1000.0, val rpm100: Double = 1000.0,
val angleRawDegree: Double = 10.0, val powerPercentRaw: Double = 10.0) {
fun validate() {
require(listOf(power100, rpm100, angleRawDegree, powerPercentRaw).all { it.isFinite() && it > 0 && it <= 100000 }) {
"Масштабы должны быть больше нуля и не больше 100000"
}
}
}
fun hit(x: Float, y: Float): Field? = when {
x in 46f..116f && y in 110f..410f -> Field.POWER
x in 174f..244f && y in 110f..410f -> Field.RPM
hypot(x - 411f, y - 264f) <= 155f -> Field.ANGLE
else -> null
}
fun requested(field: Field, x: Float, y: Float): Double? = when (field) {
Field.ANGLE -> if (hypot(x - 411f, y - 264f) < 12f) null else atan2((x - 411).toDouble(), (264 - y).toDouble()) * 180 / PI
else -> { val low = if (field == Field.RPM) -20.0 else 0.0
(120 - (y - 126) / 270 * (120 - low)).coerceIn(low, 120.0) }
}
fun raw(field: Field, value: Double, scales: Scales): Map<Parameter, Int> {
scales.validate(); require(value.isFinite())
fun limited(v: Double) = round(v).coerceIn(-32767.0, 32765.0).toInt()
if (field == Field.ANGLE) return mapOf(Parameter.ANGLE to limited(value * scales.angleRawDegree))
val power = field == Field.POWER
val factor = if (power) 1.0 else 10.0
val full = if (power) scales.power100 else scales.rpm100
val raw = limited(value / 100 * full * factor)
val percent = raw / factor / full * 100
return mapOf((if (power) Parameter.POWER else Parameter.RPM) to raw,
(if (power) Parameter.POWER_PERCENT else Parameter.RPM_PERCENT) to limited(percent * if (power) scales.powerPercentRaw else 10.0))
}
fun preview(raw: Map<Parameter, Int>, scales: Scales): Map<Field, Double> = mapOf(
Field.POWER to (raw[Parameter.POWER] ?: 0) / scales.power100 * 100,
Field.RPM to (raw[Parameter.RPM] ?: 0) / 10.0 / scales.rpm100 * 100,
Field.ANGLE to (raw[Parameter.ANGLE] ?: 0) / scales.angleRawDegree,
)
}

View File

@@ -0,0 +1,53 @@
package ru.setcorp.setprotocol.deif
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
/** Host hides system chrome; controller survives rotation and maintains the active cycle. */
@Composable
fun DeifFullScreen(controller: DeifController, connected: Boolean) {
val state by controller.state.collectAsState()
var tick by remember { mutableLongStateOf(0) }
LaunchedEffect(controller) { while (true) { delay(250); tick++ } }
BackHandler { controller.setFullScreen(false) }
Column(Modifier.fillMaxSize().background(Color(0xFF090C0E)).displayCutoutPadding()) {
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
val preview = DeifControls.preview(state.raw, state.controlScales)
@Composable fun instrument(side: Int, modifier: Modifier) {
val values = remember(state.readings, tick) { Parameter.entries.associateWith { controller.value(side, it) } }
Box(modifier, contentAlignment = Alignment.Center) {
DeifInstrument("${if (side == 0) "MASTER" else "SLAVE"} · Node ${state.displays[side].node}",
values, preview, controller::requestInstrument, Modifier.fillMaxSize())
}
}
if (maxWidth > maxHeight) Row(Modifier.fillMaxSize()) {
instrument(0, Modifier.weight(1f).fillMaxHeight())
instrument(1, Modifier.weight(1f).fillMaxHeight())
} else Column(Modifier.fillMaxSize()) {
instrument(0, Modifier.weight(1f).fillMaxWidth())
instrument(1, Modifier.weight(1f).fillMaxWidth())
}
}
Surface(color = Color(0xFF111820), contentColor = Color.White) {
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button({ controller.setFullScreen(false) }) { Text("Выйти из полного экрана") }
Button({ controller.start(false) }, enabled = connected && !state.unlocked && !state.running) { Text("Цикл") }
Button({ controller.start(true) }, enabled = connected && !state.unlocked && !state.running) { Text("Старт") }
Button({ controller.stop() }, enabled = state.running) { Text("Стоп") }
Text(if (!connected) "Нет подключения" else if (state.unlocked) "Настройки разблокированы" else if (state.running) "MASTER работает" else "Передача выключена")
}
}
}
}

View File

@@ -0,0 +1,134 @@
package ru.setcorp.setprotocol.deif
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Typeface
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.pointerInput
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
import kotlin.math.*
/** Vector port of SETGUI Propulsor.paintEvent, including its original silhouette. */
@Composable
fun DeifInstrument(title: String, values: Map<Parameter, Double?>, preview: Map<DeifControls.Field, Double>,
onRequest: (DeifControls.Field, Double) -> Unit, modifier: Modifier = Modifier) {
val request by rememberUpdatedState(onRequest)
Canvas(modifier.fillMaxWidth().aspectRatio(600f / 510f).pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
val scale = min(size.width / 600f, size.height / 510f)
val ox = (size.width - 600 * scale) / 2
val oy = (size.height - 510 * scale) / 2
fun logical(p: androidx.compose.ui.geometry.Offset) = (p.x - ox) / scale to (p.y - oy) / scale
val (x, y) = logical(down.position)
val field = DeifControls.hit(x, y)
if (field != null) {
down.consume()
DeifControls.requested(field, x, y)?.let { request(field, it) }
do {
val change = awaitPointerEvent().changes.firstOrNull { it.id == down.id } ?: break
val (px, py) = logical(change.position)
DeifControls.requested(field, px, py)?.let { request(field, it) }
change.consume()
} while (change.pressed)
}
}
}) {
val c = drawContext.canvas.nativeCanvas
val p = Paint(Paint.ANTI_ALIAS_FLAG)
fun color(hex: String) = android.graphics.Color.parseColor(hex)
fun text(x: Float, y: Float, w: Float, h: Float, value: String, size: Float = 18f, ink: String = "#e9edf4", bold: Boolean = false) {
p.style = Paint.Style.FILL; p.color = color(ink); p.textSize = size * 1.25f
p.typeface = Typeface.create("sans-serif", if (bold) Typeface.BOLD else Typeface.NORMAL)
p.textAlign = Paint.Align.CENTER
c.drawText(value, x + w / 2, y + h / 2 - (p.ascent() + p.descent()) / 2, p)
}
fun line(x1: Float, y1: Float, x2: Float, y2: Float, ink: String, width: Float = 1f) {
p.color = color(ink); p.strokeWidth = width; p.style = Paint.Style.STROKE; p.strokeCap = Paint.Cap.BUTT
c.drawLine(x1, y1, x2, y2, p)
}
fun number(key: Parameter, suffix: String = "") = values[key]?.let { "%s%s".format(java.math.BigDecimal.valueOf(it).stripTrailingZeros().toPlainString(), suffix) } ?: "—"
c.save()
c.clipRect(0f, 0f, size.width, size.height)
c.drawColor(color("#090c0e"))
val scale = min(size.width / 600, size.height / 510)
c.translate((size.width - 600 * scale) / 2, (size.height - 510 * scale) / 2); c.scale(scale, scale)
text(15f, 12f, 570f, 40f, title, 22f)
text(205f, 54f, 95f, 25f, "Задание:", 11f, "#8798ad")
text(300f, 54f, 95f, 25f, preview[DeifControls.Field.ANGLE]?.let { "%.1f°".format(it) } ?: "—°", 11f)
for ((x, field) in listOf(64f to DeifControls.Field.POWER, 192f to DeifControls.Field.RPM)) {
val power = field == DeifControls.Field.POWER
val low = if (power) 0 else -20
fun yy(v: Double) = (126 + (120 - v) / (120 - low) * 270).toFloat()
for ((a, b, ink) in listOf(Triple(low, 0, "#ae616b"), Triple(0, 100, "#657caf"), Triple(100, 120, "#b6c799"))) {
if (b > a) { p.style = Paint.Style.FILL; p.color = color(ink); c.drawRect(x, yy(b.toDouble()), x + 34, yy(a.toDouble()), p) }
}
for (v in low..120 step 20) {
val y = yy(v.toDouble()); line(x, y, x + 34, y, "#202b3a")
if (v in listOf(low, 0, 40, 80, 120)) text(x - 40, y - 12, 36f, 24f, v.toString(), 12f)
}
val percentKey = if (power) Parameter.POWER_PERCENT else Parameter.RPM_PERCENT
val percent = values[percentKey]
if (percent != null) { val y = yy(percent.coerceIn(low.toDouble(), 120.0)); line(x - 3, y, x + 38, y, "#ffffff", 4f) }
else { p.style = Paint.Style.FILL; p.color = android.graphics.Color.argb(130, 16, 21, 29); c.drawRect(x, 126f, x + 34, 396f, p) }
preview[field]?.let { val y = yy(it.coerceIn(low.toDouble(), 120.0)); line(x - 8, y, x + 42, y, "#eeef69", 3f) }
text(x - 35, 89f, 110f, 30f, number(percentKey, "%"), 17f)
text(x - 45, 410f, 128f, 28f, if (power) "POWER" else "RPM", 17f)
text(x - 52, 444f, 145f, 30f, number(if (power) Parameter.POWER else Parameter.RPM), 22f)
text(x - 40, 478f, 125f, 22f, if (power) "RAW" else "rpm", 11f, "#8798ad")
}
val cx = 411f; val cy = 264f; val radius = 125f
val ring = RectF(cx - 120, cy - 120, cx + 120, cy + 120)
p.style = Paint.Style.STROKE; p.strokeWidth = 10f; p.strokeCap = Paint.Cap.BUTT
p.color = color("#78f033"); c.drawArc(ring, -90f, 180f, false, p)
p.color = color("#ff4935"); c.drawArc(ring, -90f, -180f, false, p)
for (degree in 0 until 360 step 5) {
val a = (degree - 90) * PI / 180
val major = degree % 30 == 0
fun xx(r: Int) = cx + r * cos(a).toFloat()
fun yy(r: Int) = cy + r * sin(a).toFloat()
line(xx(if (major) 115 else 118), yy(if (major) 115 else 118), xx(if (major) 137 else 126), yy(if (major) 137 else 126), "#e9f3ff", if (major) 2.3f else 1f)
if (major) text(xx(156) - 26, yy(156) - 16, 52f, 32f, min(degree, 360 - degree).toString() + if (degree % 90 == 0) "°" else "", 17f, bold = true)
}
val measured = values[Parameter.ANGLE]
val angle = preview[DeifControls.Field.ANGLE] ?: measured
val outline = if (angle != null) "#eeef69" else "#657181"
c.save(); c.translate(cx, cy); c.rotate((angle ?: 0.0).toFloat() + 90)
p.color = color(outline); p.style = Paint.Style.STROKE; p.strokeWidth = 5.3f; p.strokeJoin = Paint.Join.ROUND; p.strokeCap = Paint.Cap.ROUND
c.save(); c.scale(radius / 315, radius / 315); c.translate(-460f, -610f)
c.drawPath(Path().apply {
moveTo(399f, 550f); cubicTo(478f, 516f, 577f, 525f, 630f, 562f)
cubicTo(679f, 594f, 672f, 632f, 626f, 661f); cubicTo(570f, 701f, 478f, 702f, 399f, 674f); close()
}, p)
c.drawPath(Path().apply {
moveTo(399f, 550f); lineTo(360f, 552f); moveTo(318f, 563f)
cubicTo(278f, 575f, 278f, 619f, 312f, 642f); lineTo(315f, 629f); cubicTo(341f, 659f, 370f, 676f, 399f, 674f)
}, p)
c.drawPath(Path().apply {
moveTo(384f, 631f); cubicTo(348f, 615f, 307f, 555f, 317f, 506f)
cubicTo(329f, 466f, 350f, 418f, 366f, 438f); cubicTo(384f, 466f, 392f, 524f, 384f, 631f); close()
}, p)
c.drawLine(318f, 499f, 384f, 631f, p)
c.drawPath(Path().apply { moveTo(315f, 629f); cubicTo(304f, 681f, 320f, 780f, 350f, 785f); cubicTo(388f, 792f, 386f, 712f, 373f, 670f) }, p)
c.restore()
if (angle != null) {
p.style = Paint.Style.FILL
c.drawPath(Path().apply { moveTo(-115f, 0f); lineTo(-77f, -15f); lineTo(-77f, 15f); close() }, p)
c.drawPath(Path().apply { moveTo(-126f, 0f); lineTo(-138f, -5f); lineTo(-138f, 5f); close() }, p)
}
c.restore()
text(300f, 435f, 225f, 36f, number(Parameter.ANGLE, "°"), 25f)
text(278f, 478f, 275f, 22f, "AZIMUTH", 11f, "#8798ad")
c.restore()
}
}

View File

@@ -0,0 +1,167 @@
package ru.setcorp.setprotocol.deif
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
/** Reusable Compose port. The host supplies only transport and persistence. */
@Composable
fun DeifPanel(controller: DeifController, connected: Boolean, channel: String) {
val state by controller.state.collectAsState()
var clock by remember { mutableLongStateOf(0) }
LaunchedEffect(controller) { while (true) { delay(250); clock++ } }
var invalidInputs by remember { mutableStateOf(emptySet<Parameter>()) }
var showRaw by remember { mutableStateOf(false) }
var controlsDialog by remember { mutableStateOf(false) }
var configuring by remember { mutableStateOf(false) }
var displaySide by remember { mutableStateOf<Int?>(null) }
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("DEIF XDi · MASTER / SLAVE", style = MaterialTheme.typography.titleLarge)
Text("Канал: $channel · ${if (connected) "подключён" else "нет подключения"}")
Row {
Checkbox(state.unlocked, { controller.unlock(it) })
Text("Разблокировать настройки", Modifier.padding(top = 12.dp))
}
Text("${state.config.mode} · Node ${state.config.node} · STD 0x%03X · %d мс".format(state.config.canId, state.config.periodMs))
OutlinedButton(onClick = { configuring = true }, enabled = state.unlocked) { Text("Настроить передачу") }
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button({ controller.start(false) }, enabled = connected && !state.unlocked && !state.running && invalidInputs.isEmpty()) { Text("Цикл") }
Button({ controller.start(true) }, enabled = connected && !state.unlocked && !state.running && invalidInputs.isEmpty()) { Text("Старт") }
OutlinedButton({ controller.stop() }, enabled = state.running) { Text("Стоп") }
OutlinedButton({ controller.setFullScreen(true) }) { Text("На весь экран") }
}
Text("Коснитесь или перетащите POWER, RPM либо винт. Жёлтый — задание, белый — показания.", style = MaterialTheme.typography.bodySmall)
val preview = remember(state.raw, state.controlScales) { DeifControls.preview(state.raw, state.controlScales) }
@Composable fun instrument(side: Int, modifier: Modifier) {
Column(modifier) {
val values = remember(state.readings, clock) { Parameter.entries.associateWith { controller.value(side, it) } }
DeifInstrument("${if (side == 0) "MASTER" else "SLAVE"} · Node ${state.displays[side].node}",
values, preview, { field, value ->
controller.requestInstrument(field, value)
invalidInputs = emptySet()
})
OutlinedButton({ displaySide = side }, enabled = state.unlocked) { Text("Настроить ${if (side == 0) "MASTER" else "SLAVE"}") }
}
}
BoxWithConstraints {
if (maxWidth >= 760.dp) Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
instrument(0, Modifier.weight(1f)); instrument(1, Modifier.weight(1f))
} else Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
instrument(0, Modifier.fillMaxWidth()); instrument(1, Modifier.fillMaxWidth())
}
}
OutlinedButton({ controlsDialog = true }, enabled = state.unlocked) { Text("Масштабы управления приборами") }
TextButton({ showRaw = !showRaw }) { Text(if (showRaw) "Скрыть RAW и выбор параметров" else "RAW и выбор параметров") }
if (showRaw) {
Parameter.entries.forEach { parameter ->
var input by remember(state.raw[parameter]) { mutableStateOf((state.raw[parameter] ?: 0).toString()) }
val raw = input.toIntOrNull()
Row(Modifier.fillMaxWidth()) {
Checkbox(parameter in state.config.enabled, { controller.select(parameter, it) })
OutlinedTextField(input, { text ->
input = text
val value = text.toIntOrNull()
invalidInputs = if (value != null && value in -32768..32767) invalidInputs - parameter else invalidInputs + parameter
if (value != null && value in -32768..32767) controller.setRaw(parameter, value)
else controller.stop("Исправьте RAW перед передачей")
}, label = { Text("${parameter.title} (${parameter.unit}) · RAW") },
isError = raw == null || raw !in -32768..32767, singleLine = true, modifier = Modifier.weight(1f))
}
Text("0x%04X:02%s · %s".format(parameter.index, if (parameter == Parameter.ANGLE) " + :07" else "",
raw?.let { "%.1f %s".format(it * parameter.scale, parameter.unit) } ?: "—"))
}
Text("AC2: физическая единица мощности не подтверждена. Угол передаётся как фактический и заданный.", style = MaterialTheme.typography.bodySmall)
}
Text(state.status)
Text("Передано: ${state.sent} · RX: ${state.received} · ошибок CAN: ${state.errors}")
Text("TX-эхо gs_usb: ${state.echoes}. Эхо не является ответом DEIF.")
if (channel.contains("gs_usb")) {
val echoStatus = remember(state.echoTimes, state.config.enabled, clock) {
state.config.enabled.flatMap { p -> (if (p == Parameter.ANGLE) listOf(2, 7) else listOf(2)).map { sub ->
"%04X:%02X — %s".format(p.index, sub, controller.echoAge(p.index, sub)?.let { if (it > 3000) "нет эха >3 с" else "$it мс" } ?: "нет эха")
} }.joinToString("\n")
}
Text(echoStatus, style = MaterialTheme.typography.bodySmall)
}
Text("Интервал: ${state.intervalMs} мс · макс. пауза: ${state.maxGapMs} мс · макс. цикл: ${state.maxCycleMs} мс")
Text("Heartbeat ${if (state.config.heartbeat) "включён · 500 мс" else "выключен"}. TX означает передачу в API адаптера, а не подтверждение DEIF.", style = MaterialTheme.typography.bodySmall)
Text(state.lastReceived)
Text("SLAVE не использует TX-эхо; отсутствие ответа допустимо. Данные скрываются через 5 с. NMT, LSS и SDO записи не отправляются.", style = MaterialTheme.typography.bodySmall)
}
if (controlsDialog) {
val scales = state.controlScales
val labels = listOf("POWER при 100%, RAW", "RPM при 100%, rpm", "Угол: RAW на 1°", "POWER%: RAW на 1%")
val values = remember { listOf(scales.power100, scales.rpm100, scales.angleRawDegree, scales.powerPercentRaw).map { mutableStateOf(it.toString()) } }
var problem by remember { mutableStateOf<String?>(null) }
AlertDialog(onDismissRequest = { controlsDialog = false }, title = { Text("Масштабы управления") }, text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
Text("Оба индикатора меняют общие задания. При работающем master значения отправятся следующим циклом.")
labels.forEachIndexed { i, label -> OutlinedTextField(values[i].value, { values[i].value = it }, label = { Text(label) }) }
problem?.let { Text(it, color = MaterialTheme.colorScheme.error) }
}
}, confirmButton = { TextButton({
runCatching { controller.configureControls(DeifControls.Scales(values[0].value.toDouble(), values[1].value.toDouble(),
values[2].value.toDouble(), values[3].value.toDouble())) }
.onSuccess { controlsDialog = false }.onFailure { problem = it.message }
}) { Text("Применить") } }, dismissButton = { TextButton({ controlsDialog = false }) { Text("Отмена") } })
}
if (configuring) {
val c = state.config
var mode by remember { mutableStateOf(c.mode) }
var node by remember { mutableStateOf(c.node.toString()) }
var id by remember { mutableStateOf(c.canId.toString(16)) }
var period by remember { mutableStateOf(c.periodMs.toString()) }
var heartbeat by remember { mutableStateOf(c.heartbeat) }
var master by remember { mutableStateOf(c.masterNode.toString()) }
var problem by remember { mutableStateOf<String?>(null) }
AlertDialog(onDismissRequest = { configuring = false }, title = { Text("Передача DEIF") }, text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
Row { DeifProtocol.Mode.entries.forEach { m -> FilterChip(mode == m, { mode = m }, label = { Text(m.name) }) } }
Text("SAM: источник; DAM: получатель. Адрес самого прибора не изменяется.")
OutlinedTextField(node, { node = it; it.toIntOrNull()?.let { n -> id = (0x200 + n).toString(16) } }, label = { Text("Node-ID (1…127)") })
OutlinedTextField(id, { id = it }, label = { Text("STD ID, HEX (201 / 20B / 30B…)") })
OutlinedTextField(period, { period = it }, label = { Text("Период, мс") })
Row { Checkbox(heartbeat, { heartbeat = it }); Text("Отправлять heartbeat") }
OutlinedTextField(master, { master = it }, label = { Text("Node-ID master для heartbeat DAM") })
problem?.let { Text(it, color = MaterialTheme.colorScheme.error) }
}
}, confirmButton = { TextButton({
runCatching { controller.configure(c.copy(mode = mode, node = node.toInt(), canId = id.removePrefix("0x").toInt(16),
periodMs = period.toLong(), heartbeat = heartbeat, masterNode = master.toInt())) }
.onSuccess { configuring = false }.onFailure { problem = it.message }
}) { Text("Применить") } }, dismissButton = { TextButton({ configuring = false }) { Text("Отмена") } })
}
displaySide?.let { side ->
val config = state.displays[side]
var node by remember { mutableStateOf(config.node.toString()) }
var id by remember { mutableStateOf(config.canId.toString(16)) }
val fields = remember { Parameter.entries.associateWith { p ->
val b = config.bindings.getValue(p)
mutableStateOf("%04X:%02X:%s".format(b.index, b.subindex, b.scale))
} }
var problem by remember { mutableStateOf<String?>(null) }
AlertDialog(onDismissRequest = { displaySide = null }, title = { Text("Привязки ${if (side == 0) "MASTER" else "SLAVE"}") }, text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
OutlinedTextField(node, { node = it }, label = { Text("Node-ID: SAM источник / DAM получатель") })
OutlinedTextField(id, { id = it }, label = { Text("SAM CAN ID, HEX") })
Text("Формат: индекс HEX:подиндекс HEX:масштаб. Например 3001:02:0.1. DAM принимается по Node-ID на RPDO1…4.")
Parameter.entries.forEach { p -> OutlinedTextField(fields.getValue(p).value, { fields.getValue(p).value = it }, label = { Text("${p.title} ${p.unit}") }) }
problem?.let { Text(it, color = MaterialTheme.colorScheme.error) }
}
}, confirmButton = { TextButton({
runCatching {
controller.configureDisplay(side, DeifDisplayConfig(node.toInt(), id.removePrefix("0x").toInt(16), fields.mapValues { (_, text) ->
val parts = text.value.split(':'); require(parts.size == 3) { "Нужны индекс:подиндекс:масштаб" }
DeifBinding(parts[0].toInt(16), parts[1].toInt(16), parts[2].toDouble())
}))
}.onSuccess { displaySide = null }.onFailure { problem = it.message }
}) { Text("Применить") } }, dismissButton = { TextButton({ displaySide = null }) { Text("Отмена") } })
}
}

View File

@@ -0,0 +1,94 @@
package ru.setcorp.setprotocol.deif
import ru.setcorp.setflash.core.CanBridgeProtocol
/** XDi-net MPDO contract shared by Android hosts; matches set_devices/deif_xdi.py. */
object DeifProtocol {
enum class Mode { SAM, DAM }
enum class Parameter(val index: Int, val title: String, val unit: String, val scale: Double) {
ANGLE(0x3001, "Угол", "°", 0.1),
RPM(0x3081, "Обороты", "rpm", 0.1),
RPM_PERCENT(0x3091, "Обороты винта", "%", 0.1),
POWER(0x3421, "Мощность AC2", "RAW", 1.0),
POWER_PERCENT(0x3431, "Мощность AC2", "%", 0.1),
}
data class Config(
val mode: Mode = Mode.SAM, val node: Int = 1, val canId: Int = 0x201,
val periodMs: Long = 100, val heartbeat: Boolean = false, val masterNode: Int = 1,
val enabled: Set<Parameter> = Parameter.entries.toSet(),
) {
fun validate() {
require(node in 1..127 && masterNode in 1..127) { "Node-ID: 1…127" }
require(periodMs in 20..60000) { "Период: 20…60000 мс" }
require(if (mode == Mode.SAM) canId == 0x200 + node || canId == 0x27F
else canId - node in listOf(0x200, 0x300, 0x400, 0x500)) { "STD ID не соответствует режиму и Node-ID" }
}
}
data class Decoded(
val summary: String, val node: Int? = null, val index: Int? = null,
val subindex: Int? = null, val raw: Int? = null, val value: Double? = null,
val unit: String = "", val mode: Mode? = null, val valid: Boolean = false,
)
fun cycle(config: Config, raw: Map<Parameter, Int>): List<CanBridgeProtocol.Frame> {
config.validate()
val frames = Parameter.entries.filter { it in config.enabled }.map { p ->
dataFrame(config, p.index, 2, raw[p] ?: 0)
}.toMutableList()
if (Parameter.ANGLE in config.enabled) frames += dataFrame(config, 0x3001, 7, raw[Parameter.ANGLE] ?: 0)
return frames
}
private fun dataFrame(c: Config, index: Int, sub: Int, raw: Int): CanBridgeProtocol.Frame {
require(raw in -32768..32767) { "RAW: −32768…32767" }
return CanBridgeProtocol.buildFrame(c.canId.toLong(), byteArrayOf(
(c.node or if (c.mode == Mode.DAM) 0x80 else 0).toByte(),
index.toByte(), (index shr 8).toByte(), sub.toByte(), raw.toByte(), (raw shr 8).toByte(), 0, 0,
), extended = false)
}
fun heartbeat(config: Config): CanBridgeProtocol.Frame {
config.validate()
val node = if (config.mode == Mode.SAM) config.node else config.masterNode
return CanBridgeProtocol.buildFrame((0x700 + node).toLong(), byteArrayOf(5), extended = false)
}
fun decode(frame: CanBridgeProtocol.Frame): Decoded {
fun reject(s: String) = Decoded("DEIF: $s")
if (frame.isExtended || frame.isRemote || frame.isError) return reject("ожидается STD data frame")
val id = frame.canId.toInt()
val data = frame.data
if (id in 0x701..0x77F) {
if (data.size != 1) return reject("heartbeat: ожидается DLC=1")
val state = when (data[0].toInt() and 255) { 0 -> "Boot-up"; 5 -> "Operational"; else -> "состояние 0x%02X".format(data[0]) }
return Decoded("Node ${id - 0x700}: $state", node = id - 0x700, valid = true)
}
if (id !in 0x201..0x57F || data.size != 8) return reject("неизвестный ID или DLC (MPDO: 8 байт)")
val node = data[0].toInt() and 127
val mode = if (data[0].toInt() and 128 != 0) Mode.DAM else Mode.SAM
if (node == 0 || !(if (mode == Mode.DAM) id - node in listOf(0x200, 0x300, 0x400, 0x500)
else id in 0x201..0x27F && (id == 0x27F || id == 0x200 + node))) return reject("адрес не соответствует COB-ID")
val index = (data[1].toInt() and 255) or ((data[2].toInt() and 255) shl 8)
val sub = data[3].toInt() and 255
val parameter = Parameter.entries.find { it.index and 0xFFF0 == index and 0xFFF0 }
if (parameter == null || index and 15 == 0 || sub !in listOf(2, 7)) return reject("неизвестный объект 0x%04X:%02X".format(index, sub))
val raw = ((data[4].toInt() and 255) or ((data[5].toInt() and 255) shl 8)).toShort().toInt()
val status = when (raw) { -32768 -> "ниже диапазона"; 32766 -> "выше диапазона"; 32767 -> "ошибка данных"; else -> null }
val scaled = sub == 2 || parameter == Parameter.ANGLE
val value = if (status == null && scaled) raw * parameter.scale else null
val unit = if (scaled) parameter.unit else "масштаб не установлен"
val padding = if (data[6] != 0.toByte() || data[7] != 0.toByte()) " · ненулевое заполнение" else ""
val summary = "$mode Node $node · 0x%04X:%02X · %s · RAW=%d".format(index, sub, parameter.title, raw) +
" · " + (status ?: value?.let { "%.1f %s".format(it, unit) } ?: unit) + padding
return Decoded(summary, node, index, sub, raw, value, unit, mode, status == null && padding.isEmpty())
}
/** Only display data and Operational heartbeat may reach this transmit path. */
fun validateTransmit(frame: CanBridgeProtocol.Frame) {
val decoded = decode(frame)
val heartbeat = frame.canId in 0x701..0x77F && frame.data.contentEquals(byteArrayOf(5))
val data = frame.data
val known = decoded.index in Parameter.entries.map { it.index } &&
(decoded.subindex == 2 || decoded.index == 0x3001 && decoded.subindex == 7)
require(!frame.isExtended && !frame.isRemote && !frame.isError &&
(heartbeat || known && decoded.raw != null && data.size == 8 && data[6] == 0.toByte() && data[7] == 0.toByte())) {
"DEIF: разрешены только данные индикации и heartbeat"
}
}
}

View File

@@ -0,0 +1,93 @@
package ru.setcorp.setprotocol.deif
import org.junit.Assert.*
import org.junit.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import ru.setcorp.setprotocol.deif.DeifControls.Field
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
@OptIn(ExperimentalCoroutinesApi::class)
class DeifControlsTest {
@Test fun fullScreenTogglePreservesLiveMasterAndIsNotRestoredOnNextLaunch() = runTest {
val sent = mutableListOf<ru.setcorp.setflash.core.CanBridgeProtocol.Frame>()
var saved = ""
val controller = DeifController(backgroundScope, { sent += it }, { saved = it })
controller.requestInstrument(Field.ANGLE, -90.0)
controller.start(true); runCurrent()
controller.setFullScreen(true)
val before = sent.size
advanceTimeBy(101); runCurrent()
assertTrue(sent.size > before)
assertTrue(controller.state.value.fullScreen)
assertEquals(-900, controller.state.value.raw[Parameter.ANGLE])
controller.setFullScreen(false)
assertTrue(controller.state.value.running)
assertFalse(controller.state.value.fullScreen)
val restored = DeifController(backgroundScope, {})
restored.restore(saved)
assertFalse(restored.state.value.fullScreen)
assertFalse(restored.state.value.running)
controller.stop()
}
@Test fun hitRegionsMatchSetguiAndLeaveBackgroundScrollable() {
assertEquals(Field.POWER, DeifControls.hit(64f, 126f))
assertEquals(Field.RPM, DeifControls.hit(192f, 396f))
assertEquals(Field.ANGLE, DeifControls.hit(411f, 120f))
assertNull(DeifControls.hit(10f, 10f))
assertNull(DeifControls.hit(250f, 450f))
}
@Test fun angleHasNorthZeroAndSignedLeftRight() {
for ((point, angle) in listOf((411f to 120f) to 0.0, (550f to 264f) to 90.0,
(411f to 410f) to 180.0, (270f to 264f) to -90.0)) {
assertEquals(angle, DeifControls.requested(Field.ANGLE, point.first, point.second)!!, 0.001)
}
assertNull(DeifControls.requested(Field.ANGLE, 411f, 264f))
}
@Test fun verticalDragsIncludeOverloadAndReverseRpm() {
assertEquals(120.0, DeifControls.requested(Field.POWER, 64f, 126f)!!, 0.001)
assertEquals(0.0, DeifControls.requested(Field.POWER, 64f, 600f)!!, 0.001)
assertEquals(-20.0, DeifControls.requested(Field.RPM, 192f, 396f)!!, 0.001)
assertEquals(120.0, DeifControls.requested(Field.RPM, 192f, -50f)!!, 0.001)
}
@Test fun gesturesUpdateAbsoluteAndPercentTogetherLikeSetgui() {
val scales = DeifControls.Scales()
assertEquals(mapOf(Parameter.POWER to 600, Parameter.POWER_PERCENT to 600), DeifControls.raw(Field.POWER, 60.0, scales))
assertEquals(mapOf(Parameter.RPM to 5000, Parameter.RPM_PERCENT to 500), DeifControls.raw(Field.RPM, 50.0, scales))
assertEquals(mapOf(Parameter.RPM to -2000, Parameter.RPM_PERCENT to -200), DeifControls.raw(Field.RPM, -20.0, scales))
assertEquals(mapOf(Parameter.ANGLE to -900), DeifControls.raw(Field.ANGLE, -90.0, scales))
}
@Test fun clampingNeverProducesStatusSentinelsAndKeepsPercentConsistent() {
val scales = DeifControls.Scales(rpm100 = 10000.0)
val raw = DeifControls.raw(Field.RPM, 120.0, scales)
assertEquals(32765, raw[Parameter.RPM])
assertEquals(328, raw[Parameter.RPM_PERCENT])
assertEquals(32.765, DeifControls.preview(raw, scales)[Field.RPM]!!, 0.0001)
assertEquals(-32767, DeifControls.raw(Field.ANGLE, -180.0, scales.copy(angleRawDegree = 1000.0))[Parameter.ANGLE])
}
@Test fun commandPreviewWorksBeforeTransmissionAndScalesPersist() = runTest {
var saved = ""
val controller = DeifController(backgroundScope, { error("Must not send while editing") }, { saved = it })
controller.unlock(true)
controller.configureControls(DeifControls.Scales(power100 = 2000.0))
controller.requestInstrument(Field.POWER, 60.0)
assertEquals(1200, controller.state.value.raw[Parameter.POWER])
assertEquals(60.0, DeifControls.preview(controller.state.value.raw, controller.state.value.controlScales)[Field.POWER]!!, 0.001)
assertNull(controller.value(0, Parameter.POWER))
val restored = DeifController(backgroundScope, {})
restored.restore(saved)
assertEquals(controller.state.value.controlScales, restored.state.value.controlScales)
}
@Test fun draggingDuringMasterChangesNextCycle() = runTest {
val sent = mutableListOf<ru.setcorp.setflash.core.CanBridgeProtocol.Frame>()
val controller = DeifController(backgroundScope, { sent += it })
controller.start(true); runCurrent()
controller.requestInstrument(Field.RPM, 50.0)
advanceTimeBy(101); runCurrent()
val rpm = sent.map(DeifProtocol::decode).filter { it.index == 0x3081 }
assertEquals(0, rpm.first().raw)
assertEquals(5000, rpm.last().raw)
assertTrue(controller.state.value.running)
controller.stop()
}
}

View File

@@ -0,0 +1,129 @@
package ru.setcorp.setprotocol.deif
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import org.junit.Assert.*
import org.junit.Test
import ru.setcorp.setflash.core.CanBridgeProtocol
import ru.setcorp.setprotocol.deif.DeifProtocol.Config
import ru.setcorp.setprotocol.deif.DeifProtocol.Mode
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
@OptIn(ExperimentalCoroutinesApi::class)
class DeifTest {
@Test fun matchesPythonSetguiReferenceCycles() {
val text = checkNotNull(javaClass.getResourceAsStream("/deif-setgui.json")).bufferedReader().use { it.readText() }
val cases = org.json.JSONArray(text)
repeat(cases.length()) { i ->
val c = cases.getJSONObject(i)
val raw = Parameter.entries.mapIndexed { n, p -> p to c.getJSONArray("raw").getInt(n) }.toMap()
val frames = DeifProtocol.cycle(Config(Mode.valueOf(c.getString("mode")), c.getInt("node"), c.getInt("canId")), raw)
frames.forEachIndexed { n, f ->
assertEquals("case $i frame $n", c.getJSONArray("frames").getString(n), f.data.joinToString("") { "%02x".format(it) })
DeifProtocol.validateTransmit(f)
}
}
}
private fun frame(id: Long, data: String, tx: Boolean = false) = CanBridgeProtocol.buildFrame(
id, CanBridgeProtocol.parseHex(data), extended = false, toCan = tx)
@Test fun setguiDamVectorAndBothAngles() {
val frames = DeifProtocol.cycle(Config(Mode.DAM, 11, 0x20B), mapOf(Parameter.POWER to 1234, Parameter.ANGLE to -900))
assertEquals(6, frames.size)
assertEquals(frame(0x20B, "8B 21 34 02 D2 04 00 00", true), frames[3])
assertEquals(frame(0x20B, "8B 01 30 02 7C FC 00 00", true), frames[0])
assertEquals(frame(0x20B, "8B 01 30 07 7C FC 00 00", true), frames[5])
assertTrue(frames.all { !it.isExtended && !it.isRemote })
}
@Test fun routingAndInputValidation() {
for (node in listOf(1, 11, 127)) for (base in listOf(0x200, 0x300, 0x400, 0x500)) {
val frames = DeifProtocol.cycle(Config(Mode.DAM, node, base + node), emptyMap())
assertTrue(frames.all { DeifProtocol.decode(it).valid })
frames.forEach(DeifProtocol::validateTransmit)
}
assertTrue(DeifProtocol.decode(frame(0x27F, "0B 81 30 02 D2 04 00 00")).valid)
assertFalse(DeifProtocol.decode(frame(0x201, "0B 81 30 02 D2 04 00 00")).valid)
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.cycle(Config(node = 0), emptyMap()) }
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.cycle(Config(), mapOf(Parameter.RPM to 32768)) }
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.cycle(Config(Mode.SAM, 11, 0x30B), emptyMap()) }
}
@Test fun decoderScalesStatusesAndRejectsUnknownFrames() {
assertEquals(-90.0, DeifProtocol.decode(frame(0x201, "01 01 30 07 7C FC 00 00")).value!!, 0.001)
assertEquals(123.4, DeifProtocol.decode(frame(0x201, "01 81 30 02 D2 04 00 00")).value!!, 0.001)
assertEquals("RAW", DeifProtocol.decode(frame(0x201, "01 21 34 02 D2 04 00 00")).unit)
assertNull(DeifProtocol.decode(frame(0x201, "01 81 30 07 D2 04 00 00")).value)
listOf("00 80", "FE 7F", "FF 7F").forEach { raw ->
val decoded = DeifProtocol.decode(frame(0x201, "01 81 30 02 $raw 00 00"))
assertFalse(decoded.valid); assertNull(decoded.value)
}
assertFalse(DeifProtocol.decode(frame(0x201, "01 81 30 02 00 00 00 01")).valid)
assertFalse(DeifProtocol.decode(frame(0x201, "01 81 30 02 00 00 00 00").copy(flags = CanBridgeProtocol.FLAG_IDE)).valid)
assertFalse(DeifProtocol.decode(frame(0x201, "01 81 30")).valid)
}
@Test fun transmitGuardRejectsConfigurationAndBadAddresses() {
listOf(frame(0, "01 0B"), frame(0x60B, "23 00 20 00 01 00 00 00"), frame(0x7E5, "04"),
frame(0x70B, "00"), frame(0x201, "0B 81 30 02 00 00 00 00")).forEach {
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.validateTransmit(it) }
}
DeifProtocol.validateTransmit(DeifProtocol.heartbeat(Config()))
assertEquals(0x701L, DeifProtocol.heartbeat(Config(Mode.DAM, 11, 0x20B)).canId)
}
@Test fun heartbeatHasIndependentPeriodAndStopsOnUnlock() = runTest {
val sent = mutableListOf<CanBridgeProtocol.Frame>()
val controller = DeifController(backgroundScope, { sent += it }, now = { testScheduler.currentTime })
controller.unlock(true); controller.configure(Config(periodMs = 1000, heartbeat = true)); controller.unlock(false)
controller.start(true); runCurrent()
assertEquals(6, sent.count { it.data.size == 8 }); assertEquals(1, sent.count { it.data.size == 1 })
advanceTimeBy(501); runCurrent()
assertEquals(6, sent.count { it.data.size == 8 }); assertEquals(2, sent.count { it.data.size == 1 })
controller.unlock(true)
val count = sent.size; advanceTimeBy(2000); runCurrent(); assertEquals(count, sent.size)
assertFalse(controller.state.value.running)
}
@Test fun singleCycleHasNoHeartbeatAndSelectionStopsRepeat() = runTest {
val sent = mutableListOf<CanBridgeProtocol.Frame>()
val controller = DeifController(backgroundScope, { sent += it })
controller.unlock(true); controller.configure(Config(heartbeat = true, enabled = setOf(Parameter.RPM))); controller.unlock(false)
controller.start(false); runCurrent()
assertEquals(1, sent.size); assertEquals(8, sent.single().data.size)
assertFalse(controller.state.value.running)
controller.start(true); runCurrent(); controller.select(Parameter.RPM, false)
val count = sent.size; advanceTimeBy(1500); runCurrent(); assertEquals(count, sent.size)
}
@Test fun failedTransportStopsBothTimers() = runTest {
var calls = 0
val controller = DeifController(backgroundScope, { calls++; error("disconnected") })
controller.start(true); runCurrent(); advanceTimeBy(1000); runCurrent()
assertEquals(1, calls); assertFalse(controller.state.value.running)
assertEquals(0L, controller.state.value.sent)
}
@Test fun slaveNeverUsesLocalTxOrEchoAndExpires() = runTest {
var time = 0L
val controller = DeifController(backgroundScope, {}, now = { time })
controller.setRaw(Parameter.RPM, 1234); controller.start(false); runCurrent()
assertEquals(123.4, controller.value(0, Parameter.RPM)!!, 0.001)
assertNull(controller.value(1, Parameter.RPM))
controller.receive(frame(0x201, "01 81 30 02 D2 04 00 00", true))
assertNull(controller.value(1, Parameter.RPM)); assertEquals(1L, controller.state.value.echoes)
controller.receive(frame(0x201, "01 81 30 02 D2 04 00 00"))
assertEquals(123.4, controller.value(1, Parameter.RPM)!!, 0.001)
time = 5001; assertNull(controller.value(1, Parameter.RPM))
controller.receive(frame(0x201, "01 81 30 02 FF 7F 00 00"))
assertNull(controller.value(1, Parameter.RPM))
}
@Test fun settingsRestoreIsLockedAndStoppedAndKeepsBindings() = runTest {
var saved = ""
val controller = DeifController(backgroundScope, {}, { saved = it })
controller.unlock(true); controller.configure(Config(Mode.DAM, 11, 0x30B, heartbeat = true))
controller.configureDisplay(1, DeifDisplayConfig(11, 0x30B))
controller.setRaw(Parameter.POWER, 1234)
val restored = DeifController(backgroundScope, {})
restored.restore(saved)
assertEquals(controller.state.value.config, restored.state.value.config)
assertEquals(11, restored.state.value.displays[1].node)
assertEquals(1234, restored.state.value.raw[Parameter.POWER])
assertFalse(restored.state.value.running); assertFalse(restored.state.value.unlocked)
restored.receive(frame(0x30B, "8B 21 34 02 D2 04 00 00"))
assertEquals(1234.0, restored.value(1, Parameter.POWER)!!, 0.001)
}
}

View File

@@ -7,9 +7,6 @@
#include "set_plot.h"
#include <math.h>
/* Allow sample-level X navigation in multi-million-point recordings. */
#define MIN_VIEW_WIDTH (1.0 / 10000000)
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); }
static int finite_values(const double *v, size_t n) {
size_t i;
@@ -26,12 +23,12 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
if (op == SET_PLOT_TRANSFORM) {
double w, h, fx, fy;
if (!finite_values(v, 4) || v[2] < MIN_VIEW_WIDTH || v[2] > 1 ||
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 ||
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0;
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
w = clamp(v[2] / v[4], MIN_VIEW_WIDTH, 1);
w = clamp(v[2] / v[4], 1.0/128, 1);
h = clamp(v[3] / v[5], 1.0/128, 1);
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1);
out[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);

View File

@@ -40,7 +40,7 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
size_t i,j,k,n=0; unsigned pass;
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000
||output_count<2||output_count>1000000||work_count<14*count+128
||output_count<2||output_count>10000||work_count<14*count+128
||method<0||method>3||(endpoint!=0&&endpoint!=1)) return 1;
pairs=(point *)work; x=work+2*count; y=x+count; d=y+count; h=d+count;
s=h+count; q=s+count; column=q+6*count;

View File

@@ -0,0 +1,722 @@
[
{
"mode": "SAM",
"node": 1,
"canId": 513,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"010130027cfc0000",
"01813002d2040000",
"01913002e8030000",
"01213402d2040000",
"01313402f4010000",
"010130077cfc0000"
]
},
{
"mode": "SAM",
"node": 1,
"canId": 513,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0101300200800000",
"01813002ffff0000",
"0191300200000000",
"01213402fe7f0000",
"01313402ff7f0000",
"0101300700800000"
]
},
{
"mode": "SAM",
"node": 1,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"010130027cfc0000",
"01813002d2040000",
"01913002e8030000",
"01213402d2040000",
"01313402f4010000",
"010130077cfc0000"
]
},
{
"mode": "SAM",
"node": 1,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0101300200800000",
"01813002ffff0000",
"0191300200000000",
"01213402fe7f0000",
"01313402ff7f0000",
"0101300700800000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 523,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"0b0130027cfc0000",
"0b813002d2040000",
"0b913002e8030000",
"0b213402d2040000",
"0b313402f4010000",
"0b0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 523,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0b01300200800000",
"0b813002ffff0000",
"0b91300200000000",
"0b213402fe7f0000",
"0b313402ff7f0000",
"0b01300700800000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"0b0130027cfc0000",
"0b813002d2040000",
"0b913002e8030000",
"0b213402d2040000",
"0b313402f4010000",
"0b0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0b01300200800000",
"0b813002ffff0000",
"0b91300200000000",
"0b213402fe7f0000",
"0b313402ff7f0000",
"0b01300700800000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"7f0130027cfc0000",
"7f813002d2040000",
"7f913002e8030000",
"7f213402d2040000",
"7f313402f4010000",
"7f0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"7f01300200800000",
"7f813002ffff0000",
"7f91300200000000",
"7f213402fe7f0000",
"7f313402ff7f0000",
"7f01300700800000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"7f0130027cfc0000",
"7f813002d2040000",
"7f913002e8030000",
"7f213402d2040000",
"7f313402f4010000",
"7f0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"7f01300200800000",
"7f813002ffff0000",
"7f91300200000000",
"7f213402fe7f0000",
"7f313402ff7f0000",
"7f01300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 513,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 513,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 769,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 769,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1025,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1025,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1281,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1281,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 523,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 523,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 779,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 779,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1035,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1035,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1291,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1291,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 895,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 895,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1151,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1151,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1407,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1407,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
}
]

View File

@@ -4,7 +4,7 @@
{"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]},
{"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]},
{"name":"pan_clamp","op":0,"input":[0.25,0.25,0.5,0.5,1,1,20,-20,0.5,0.5],"output":[0,0.5,0.5,0.5]},
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.4995,0.49609375,0.001,0.0078125]},
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.49609375,0.49609375,0.0078125,0.0078125]},
{"name":"ignore_invalid_zoom","op":0,"input":[0.25,0.25,0.5,0.5,0,1,0,0,0.5,0.5],"output":[0.25,0.25,0.5,0.5]},
{"name":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},

View File

@@ -7,7 +7,6 @@ Device Name — отображаемое имя адреса, а не допол
| DeviceType | DeviceID | Device Name | Назначение |
|---|---|---|---|
| `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы |
| `0x3` | `0x7` | **F28335-M95M02** | Загрузка прошивки по ProtoCAN в EEPROM M95M02 платы TMS320F28335; ProductType `0x2833` |
| `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов |
| `0x7` | `0xD` | configurator | Существующий конфигуратор |
| `0x7` | `0xF` | KONOR / SETTINGS | Существующий профиль SETCAN и привязка датчиков |

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -112,78 +112,3 @@ CAN ID = 0x13590702
Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты
находятся в [examples/test-vectors.json](examples/test-vectors.json).
## GAS: сквозная карта УКСС и профиль 167/v1
УКСС назначены десятичные ID **0…11999**, блоки по 1000 слов:
`GAS_ID = номер_узла × 1000 + смещение`. Блок 0 — общий резерв;
узлы 1…11 и все границы описаны в
[GAS_ADDRESS_ALLOCATION_167.md](GAS_ADDRESS_ALLOCATION_167.md).
Диапазон 12000…65535 свободен для последующего распределения.
УМП 1 (узел 5) назначены ID 5000…5127, УМП 2 (узел 6) —
6000…6127. Смещения 128…999 зарезервированы. Полный словарь 128 слов,
ограничения LOAD, биты Commands/LOAD_STATUS и соответствие Modbus приведены в
[GAS_MODBUS_MAP_167.md](GAS_MODBUS_MAP_167.md).
Новые ID пока назначены только в документации. Действующий профиль 167/v1
использует Modbus 0x0000…0x007F и окно GAS 0x5000…0x507F;
например, LOAD_CAN_TIMEOUT: Modbus 0x0052, GAS 0x5052,
новые ID 5082/6082. Десятичный ID 5000 не равен hex-адресу 0x5000.
Служебный CAN УМП с маркером 0x1F00 — отдельный транспорт;
его пакет нельзя интерпретировать как обычный payload ProtoCAN GAS.
Словарь и lookup находятся в `python/protocan/gas_167.py` и
`gas_167_registers.json` проекта templates. Парсер обычных кадров GAS
показывает назначенные имена, узел, старые адреса и статус для запросов
и каждого слова ответа; неизвестные и резервные адреса сохраняются raw.
Общая сводка `Decoded.summary` содержит имя каждого назначенного параметра,
УМП 1/2, десятичный ID, hex-адрес и значение в dec/hex. Поэтому имена видны
непосредственно в колонке «Расшифровка» всех клиентов общего декодера:
онлайн-монитор, трассировка, фильтр кадров, ручной разбор и DevBoard_V1.
Полный текст длинной строки доступен во всплывающей подсказке.
Тип сообщения отображается как `GAS (General Address Space)`.
Обычные демо-потоки шины и DevBoard также включают назначенные адреса УМП.
Низкие Modbus-адреса и старое окно не определяют профиль прибора автоматически.
DATA обычного ProtoCAN GAS — до четырёх u16 little-endian; Modbus RTU
передаёт каждое слово старшим байтом вперёд. GUI использует этот же словарь
через совместимый импорт, без собственной реализации карты.
### Демо УМП в SETGUI
Общий список «Протокол» позволяет выбрать `ProtoCAN / GAS`,
`УМП / Balsam 167 (legacy CAN)`, карту `GAS 167/v1` отдельно для УМП 1/2
или `УМП / ProtoCAN GAS — новые ID 5000/6000`.
Выбор перерисовывает уже полученные кадры. Для старой карты номер УМП
выбирается оператором и обозначается «выбран вручную»; низкие адреса
0…127 и окно 0x5000…0x507F сами по себе номер узла не кодируют.
Этот режим накладывает словарь на слова ProtoCAN GAS/Modbus Holding;
он не является декодером служебных CAN-пакетов с маркером 0x1F00.
Профиль Balsam принимает только собственный формат и показывает
несоответствие для чужих кадров. В сводке Commands показаны активные
биты с названиями команд; это не подтверждение их выполнения.
В режиме «Авто» legacy-ID Balsam проверяются первыми. Для корректного
ProtoCAN GAS с целыми словами все адреса кадра сверяются с назначенными
диапазонами УМП. Если все они принадлежат одному узлу, поля Device Name
и Msg Type показывают УМП 1/2; основание выбора записывается в подробности
«Автоопределение». Это распознавание назначенной карты, а не подтверждение
её поддержки прошивкой. Для старых/неизвестных адресов, смешанных диапазонов
и некорректного payload автоматический выбор УМП не выполняется;
сводка сообщает «профиль/узел не определены». Номер Device в ProtoCAN сам
по себе не считается номером УМП. «Демо УМП» запускается с режимом «Авто».
Открыть `CAN → Шина CAN / мост → Настройки → Демо УМП`.
Генератор `python/set_devices/bus_demo.py:UmpDemoSource` моделирует два
узла с демонстрационными адресами `Device Type=3`, `Device=5/6`.
Он передаёт PULSE раз в секунду и четыре GAS-слова каждого узла каждые
250 мс: ID 5000…5127 и 6000…6127, полный обход за 8 секунд.
Это имитация назначенной карты, а не эмуляция текущего транспорта 167/v1.
Кадры проходят обычное кадрирование AA 55, CRC и общий парсер ProtoCAN.
Их можно смотреть в онлайн-мониторе, трассировке и адресном пространстве;
в буфере сессии и его CSV источник обозначен `DEMO УМП`.
Значения синтетические, в формате raw u16. LOAD_STATUS показывает RUN
(бит 3), LOAD_ATTEMPTS — 0, LOAD_CAN_TIMEOUT — 1000 мс.
Кнопка «Стоп демо УМП» останавливает поток. Для запуска нужно отключить
COM-порт и CAN-снифер; генератор работает локально без передачи в оборудование.

View File

@@ -597,11 +597,11 @@ CRC32 обнаруживает случайное повреждение, но
<td class="x14">&nbsp;</td>
<td colspan="8" class="x96">64 битовых адресса</td>
</tr>
<tr style="height:72px">
<tr style="height:17px">
<td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td>
<td colspan="3" class="x131">0x3</td>
<td colspan="4" class="x136">0x0 - 0xF устройств<br>0x7: F28335-M95M02<br>ProductType: 0x2833</td>
<td colspan="4" class="x136">0x0 - 0xF устройств</td>
<td colspan="12" class="x138">0x3. Установка времени</td>
<td colspan="4" class="x140">&nbsp;</td>
<td class="x14">&nbsp;</td>

View File

@@ -0,0 +1,13 @@
# Перенос контроля версии прошивки
В templates используются общий модуль `c/firmware-info` и общий публикатор `tools/firmware-publish`. При переносе подключаются конфигурация контроллера, генерация build ID, обработчик запроса версии и настройки выпуска.
- [Пошаговое подключение модуля версии](c/firmware-info/PORTING.md).
- [BAT-файлы precompile и postcompile для CCS и Keil](tools/firmware-build/README.md).
- [Настройка подготовки и публикации образов](tools/firmware-publish/PORTING.md).
В режиме CCS `--identity` precompile создаёт заголовок версии и Git-метаданные, а postcompile преобразует OUT в SCI8 HEX/BIN и создаёт manifest и SHA-256. В прежнем режиме postcompile запускает SETGUI preflight готового образа. Для каждого проекта задаются явные пути; алгоритмы остаются в templates.
[Копирование BIN, manifest и SHA-256 в каталог SETGUI после postcompile](tools/firmware-build/README.md#4-копирование-результата-в-каталог-setgui).
Для C2000 с 16-битным char требуется отдельная адаптация байтового контракта. BAT-файлы сами по себе эту адаптацию не выполняют.

View File

@@ -62,7 +62,7 @@ class BalsamFrame:
"%s=0x%04X (%d)" % (item.name or "R%04X" % item.address,
item.value, item.signed_value)
+ (" — " + self.command_description(item.value)
if item.address == 0x7F else "")
if not self.from_device and item.address == 0x7F else "")
for item in self.registers
) or "нет отмеченных регистров"
return "BALZAM · %s · %s · %s" % (self.route, direction, values)

View File

@@ -37,8 +37,6 @@ class CanBootTransfer:
def __init__(self, image, target: CanBootTarget):
if not 0 < len(image.data) <= 512 * 1024:
raise ValueError("ProtoCAN Boot: размер образа 1…512 КиБ")
if target.product_type == 0x2833 and len(image.data) > 0x1FE00:
raise ValueError("F28335 M95M02 A/B: образ не должен превышать 130560 байт")
if not 0 <= image.version <= 0xFFFFFFFF:
raise ValueError("Версия образа вне диапазона u32")
self.image, self.target = image, target
@@ -48,10 +46,6 @@ class CanBootTransfer:
self.finished = False
self._window_end = 0
@property
def response_timeout_ms(self):
return 15000 if self.stage in ("enter", "verify", "commit") else 1500
@property
def percent(self):
return min(100, self.next_block * 8 * 100 // len(self.image.data))
@@ -134,93 +128,4 @@ class CanBootTransfer:
self.stage = "reboot"
return self._control(9), "Перезапуск прибора"
self.stage, self.finished = "finished", True
if self.target.product_type == 0x2833:
return [], "Образ принят; после перезапуска приложение подтвердит пробный запуск"
return [], "Прошивка ProtoCAN Boot завершена"
class CanBootServices:
"""Read-only passport queries and explicit session service commands.
Replies use message D, body=(page<<8)|command. IDENTIFY/discovery reply body=1.
All multibyte values are little endian; version is major:8/minor:8/patch:16.
"""
def __init__(self, target: CanBootTarget):
self.target = target
def request(self, command: int, page: int = 0):
if command not in (1, 8, 11, 12, 13, 14, 15):
raise ValueError("Unsupported service command")
if not 0 <= page <= 255 or (page and command not in (14, 15)):
raise ValueError("Invalid service page")
t = self.target
payload = bytes([page]) if command in (14, 15) else b""
return build_frame(ProtoCanId.build(1, 0, t.device_type, t.device, 9,
t.session_id << 8 | command), payload, to_can=True)
@staticmethod
def discover():
return build_frame(ProtoCanId.build(1, 0, 7, 15, 13, 0), b"", to_can=True)
def decode(self, frame, *, discovery=False):
p = ProtoCanId.parse(frame.can_id)
if (frame.to_can or not frame.ide or frame.rtr or frame.is_error or p.pm != 1
or p.msg_type not in (12, 13) or len(frame.data) != 8):
return None
if discovery:
if p.msg_type != 13 or p.body != 1:
return None
elif (p.device_type, p.device) != (self.target.device_type, self.target.device):
return None
if p.msg_type == 12:
if p.body >> 8 != self.target.session_id:
return None
status, slot, next_block, crc32 = struct.unpack("<BBHI", frame.data)
return {"kind": "status", "device_type": p.device_type, "device": p.device,
"command": p.body & 255, "status": status, "target_slot": slot,
"next_block": next_block, "crc32": crc32}
command, page = p.body & 255, p.body >> 8
d = frame.data
result = {"kind": "passport", "device_type": p.device_type, "device": p.device, "command": command, "page": page}
if command == 1 and page == 0:
product, hardware, protocol, version = struct.unpack("<HBBI", d)
result.update(product_type=product, hardware_revision=hardware, protocol_version=protocol,
firmware_version=version, version=(version >> 24, (version >> 16) & 255, version & 65535))
elif command == 12 and page == 0:
result.update(protocol_version=d[0], state=d[1], active_slot=d[2], target_slot=d[3],
session_id=d[4], slot_count=d[5], last_status=d[6])
elif command == 13 and page == 0:
flags, capacity = struct.unpack("<II", d)
result.update(capabilities=flags, slot_capacity=capacity)
elif command == 14:
result["name_fragment"] = d.split(b"\0", 1)[0].decode("ascii", errors="replace")
elif command == 15:
if page == 0:
size, page_size, mode, running = struct.unpack("<IHBB", d)
result.update(memory_bytes=size, page_bytes=page_size, bootloader=bool(mode), running_slot=running)
elif page == 1:
result["slot_offsets"] = struct.unpack("<II", d)
elif page == 2:
active, pending, attempted, healthy, timeout = struct.unpack("<BBBBI", d)
result.update(active_slot=active, pending_slot=pending, trial_attempted=bool(attempted),
storage_healthy=bool(healthy), session_timeout_ms=timeout)
elif page == 3:
result["parameters_offset"], result["parameters_bytes"] = struct.unpack("<II", d)
else:
return None
else:
return None
return result
class CanBootPingExchange:
"""PING adapter with the same request/feed shape as the SETProtocol client."""
def __init__(self, target: CanBootTarget):
self.services = CanBootServices(target)
def request(self):
return [self.services.request(12)]
def feed(self, frame):
reply = self.services.decode(frame)
return reply if reply and reply["kind"] == "passport" and reply["command"] == 12 else None

View File

@@ -1,51 +0,0 @@
"""GAS 167/v1 dictionary and assigned UKSS IDs; values remain raw u16."""
import json
from pathlib import Path
REGISTERS = tuple(json.loads(Path(__file__).with_name('gas_167_registers.json').read_text(encoding='utf-8'))['registers'])
BY_ADDRESS = {entry['address']: entry for entry in REGISTERS}
BY_ASSIGNED_ID = {entry[key]: (node, entry) for node, key in ((5, 'ump1_id'), (6, 'ump2_id')) for entry in REGISTERS}
COMMAND_NAMES = (
'cTestLamp — проверка ламп', 'cDefParam — параметры по умолчанию',
'cSaveParam — сохранение параметров', 'cLoadParam — загрузка параметров',
'cTermoCal — термокалибровка', 'cKoefCalc — расчёт коэффициентов',
'cUMPreset — останов заряда', 'cUMPstart — пуск заряда',
'cInitDac — инициализация ЦАП', 'cCalibrDac — калибровка ЦАП',
'cSecretBt', 'cLiteFire', 'cRawMeat', 'бит 13 (не назначен)',
'бит 14 (не назначен)', 'cReset — сброс',
)
def describe_word(offset, value):
"""Describe active bits using the selected LOAD register map."""
if offset == 127:
return ', '.join(name for bit, name in enumerate(COMMAND_NAMES)
if value & (1 << bit)) or 'нет активных команд'
if offset == 124:
names = ('LOAD_FAULT — ошибка пуска', 'нет свежего напряжения ЗПТ',
'LOAD_RELOAD — сброс перед пуском', 'LOAD_RUN — работа рампы',
'имитация потери ЗПТ', 'имитация 0 В')
return ', '.join(names[bit] if bit < len(names) else 'бит %d' % bit
for bit in range(16) if value & (1 << bit)) or 'нет активных флагов LOAD'
return ''
def metadata(address):
"""Metadata for the explicitly selected legacy Modbus profile."""
index = address if 0 <= address < 128 else address - 0x5000
if not 0 <= index < 128:
return {}
entry = REGISTERS[index]
return {'name': entry['description'], 'gas_id': f"{entry['ump1_id']} / {entry['ump2_id']}"}
def assigned_metadata(address):
"""Only assigned global IDs; reserved offsets have no parameter name."""
match = BY_ASSIGNED_ID.get(address)
if match is None:
return {}
node, entry = match
return {'node': node, 'name': entry['description'], 'access': entry['access'],
'modbus': entry['modbus'], 'legacy_address': entry['address'],
'status': 'назначен; прошивка 167/v1 ещё не обслуживает'}

View File

@@ -1,1030 +0,0 @@
{
"profile": "167/v1",
"source_commit": "378e06a",
"registers": [
{
"modbus": 0,
"address": 20480,
"ump1_id": 5000,
"ump2_id": 6000,
"description": "sens_error[0]; InputRep0/1/2 — биты A/B/C",
"access": "RW"
},
{
"modbus": 1,
"address": 20481,
"ump1_id": 5001,
"ump2_id": 6001,
"description": "sens_error[1]",
"access": "RW"
},
{
"modbus": 2,
"address": 20482,
"ump1_id": 5002,
"ump2_id": 6002,
"description": "sens_error[2]",
"access": "RW"
},
{
"modbus": 3,
"address": 20483,
"ump1_id": 5003,
"ump2_id": 6003,
"description": "sens_error[3]",
"access": "RW"
},
{
"modbus": 4,
"address": 20484,
"ump1_id": 5004,
"ump2_id": 6004,
"description": "sens_error[4]",
"access": "RW"
},
{
"modbus": 5,
"address": 20485,
"ump1_id": 5005,
"ump2_id": 6005,
"description": "sens_error[5]",
"access": "RW"
},
{
"modbus": 6,
"address": 20486,
"ump1_id": 5006,
"ump2_id": 6006,
"description": "sens_error[6]",
"access": "RW"
},
{
"modbus": 7,
"address": 20487,
"ump1_id": 5007,
"ump2_id": 6007,
"description": "sens_error[7]",
"access": "RW"
},
{
"modbus": 8,
"address": 20488,
"ump1_id": 5008,
"ump2_id": 6008,
"description": "sens_error[8]",
"access": "RW"
},
{
"modbus": 9,
"address": 20489,
"ump1_id": 5009,
"ump2_id": 6009,
"description": "sens_error[9]",
"access": "RW"
},
{
"modbus": 10,
"address": 20490,
"ump1_id": 5010,
"ump2_id": 6010,
"description": "sens_error[10]",
"access": "RW"
},
{
"modbus": 11,
"address": 20491,
"ump1_id": 5011,
"ump2_id": 6011,
"description": "sens_error[11]",
"access": "RW"
},
{
"modbus": 12,
"address": 20492,
"ump1_id": 5012,
"ump2_id": 6012,
"description": "sens_error[12]",
"access": "RW"
},
{
"modbus": 13,
"address": 20493,
"ump1_id": 5013,
"ump2_id": 6013,
"description": "sens_error[13]",
"access": "RW"
},
{
"modbus": 14,
"address": 20494,
"ump1_id": 5014,
"ump2_id": 6014,
"description": "sens_error[14]",
"access": "RW"
},
{
"modbus": 15,
"address": 20495,
"ump1_id": 5015,
"ump2_id": 6015,
"description": "sens_error[15]",
"access": "RW"
},
{
"modbus": 16,
"address": 20496,
"ump1_id": 5016,
"ump2_id": 6016,
"description": "sens_error[16]; Inputs",
"access": "RW"
},
{
"modbus": 17,
"address": 20497,
"ump1_id": 5017,
"ump2_id": 6017,
"description": "sens_error[17]",
"access": "RW"
},
{
"modbus": 18,
"address": 20498,
"ump1_id": 5018,
"ump2_id": 6018,
"description": "sens_error[18]",
"access": "RW"
},
{
"modbus": 19,
"address": 20499,
"ump1_id": 5019,
"ump2_id": 6019,
"description": "sens_error[19]",
"access": "RW"
},
{
"modbus": 20,
"address": 20500,
"ump1_id": 5020,
"ump2_id": 6020,
"description": "sens_error[20]",
"access": "RW"
},
{
"modbus": 21,
"address": 20501,
"ump1_id": 5021,
"ump2_id": 6021,
"description": "sens_error[21]",
"access": "RW"
},
{
"modbus": 22,
"address": 20502,
"ump1_id": 5022,
"ump2_id": 6022,
"description": "sens_error[22]; Buttons",
"access": "RW"
},
{
"modbus": 23,
"address": 20503,
"ump1_id": 5023,
"ump2_id": 6023,
"description": "sens_error[23]; Jumpers",
"access": "RW"
},
{
"modbus": 24,
"address": 20504,
"ump1_id": 5024,
"ump2_id": 6024,
"description": "sens_data[0]",
"access": "RW"
},
{
"modbus": 25,
"address": 20505,
"ump1_id": 5025,
"ump2_id": 6025,
"description": "sens_data[1]",
"access": "RW"
},
{
"modbus": 26,
"address": 20506,
"ump1_id": 5026,
"ump2_id": 6026,
"description": "sens_data[2]",
"access": "RW"
},
{
"modbus": 27,
"address": 20507,
"ump1_id": 5027,
"ump2_id": 6027,
"description": "sens_data[3]",
"access": "RW"
},
{
"modbus": 28,
"address": 20508,
"ump1_id": 5028,
"ump2_id": 6028,
"description": "sens_data[4]; LOAD: расчётный выход 4–20 мА, raw ×10",
"access": "RW"
},
{
"modbus": 29,
"address": 20509,
"ump1_id": 5029,
"ump2_id": 6029,
"description": "sens_data[5]; LOAD: измеренный вход 4–20 мА, raw ×10",
"access": "RW"
},
{
"modbus": 30,
"address": 20510,
"ump1_id": 5030,
"ump2_id": 6030,
"description": "sens_data[6]",
"access": "RW"
},
{
"modbus": 31,
"address": 20511,
"ump1_id": 5031,
"ump2_id": 6031,
"description": "sens_data[7]",
"access": "RW"
},
{
"modbus": 32,
"address": 20512,
"ump1_id": 5032,
"ump2_id": 6032,
"description": "sens_data[8]",
"access": "RW"
},
{
"modbus": 33,
"address": 20513,
"ump1_id": 5033,
"ump2_id": 6033,
"description": "sens_data[9]",
"access": "RW"
},
{
"modbus": 34,
"address": 20514,
"ump1_id": 5034,
"ump2_id": 6034,
"description": "sens_data[10]",
"access": "RW"
},
{
"modbus": 35,
"address": 20515,
"ump1_id": 5035,
"ump2_id": 6035,
"description": "sens_data[11]",
"access": "RW"
},
{
"modbus": 36,
"address": 20516,
"ump1_id": 5036,
"ump2_id": 6036,
"description": "sens_data[12]",
"access": "RW"
},
{
"modbus": 37,
"address": 20517,
"ump1_id": 5037,
"ump2_id": 6037,
"description": "sens_data[13]",
"access": "RW"
},
{
"modbus": 38,
"address": 20518,
"ump1_id": 5038,
"ump2_id": 6038,
"description": "sens_data[14]",
"access": "RW"
},
{
"modbus": 39,
"address": 20519,
"ump1_id": 5039,
"ump2_id": 6039,
"description": "sens_data[15]",
"access": "RW"
},
{
"modbus": 40,
"address": 20520,
"ump1_id": 5040,
"ump2_id": 6040,
"description": "sens_data[16]",
"access": "RW"
},
{
"modbus": 41,
"address": 20521,
"ump1_id": 5041,
"ump2_id": 6041,
"description": "sens_data[17]",
"access": "RW"
},
{
"modbus": 42,
"address": 20522,
"ump1_id": 5042,
"ump2_id": 6042,
"description": "sens_data[18]",
"access": "RW"
},
{
"modbus": 43,
"address": 20523,
"ump1_id": 5043,
"ump2_id": 6043,
"description": "sens_data[19]",
"access": "RW"
},
{
"modbus": 44,
"address": 20524,
"ump1_id": 5044,
"ump2_id": 6044,
"description": "sens_data[20]",
"access": "RW"
},
{
"modbus": 45,
"address": 20525,
"ump1_id": 5045,
"ump2_id": 6045,
"description": "sens_data[21]",
"access": "RW"
},
{
"modbus": 46,
"address": 20526,
"ump1_id": 5046,
"ump2_id": 6046,
"description": "sens_data[22]",
"access": "RW"
},
{
"modbus": 47,
"address": 20527,
"ump1_id": 5047,
"ump2_id": 6047,
"description": "sens_data[23]",
"access": "RW"
},
{
"modbus": 48,
"address": 20528,
"ump1_id": 5048,
"ump2_id": 6048,
"description": "sens_hi_edge[0]",
"access": "RW"
},
{
"modbus": 49,
"address": 20529,
"ump1_id": 5049,
"ump2_id": 6049,
"description": "sens_hi_edge[1]",
"access": "RW"
},
{
"modbus": 50,
"address": 20530,
"ump1_id": 5050,
"ump2_id": 6050,
"description": "sens_hi_edge[2]",
"access": "RW"
},
{
"modbus": 51,
"address": 20531,
"ump1_id": 5051,
"ump2_id": 6051,
"description": "sens_hi_edge[3]",
"access": "RW"
},
{
"modbus": 52,
"address": 20532,
"ump1_id": 5052,
"ump2_id": 6052,
"description": "sens_hi_edge[4]",
"access": "RW"
},
{
"modbus": 53,
"address": 20533,
"ump1_id": 5053,
"ump2_id": 6053,
"description": "sens_hi_edge[5]",
"access": "RW"
},
{
"modbus": 54,
"address": 20534,
"ump1_id": 5054,
"ump2_id": 6054,
"description": "sens_hi_edge[6]",
"access": "RW"
},
{
"modbus": 55,
"address": 20535,
"ump1_id": 5055,
"ump2_id": 6055,
"description": "sens_hi_edge[7]",
"access": "RW"
},
{
"modbus": 56,
"address": 20536,
"ump1_id": 5056,
"ump2_id": 6056,
"description": "sens_hi_edge[8]",
"access": "RW"
},
{
"modbus": 57,
"address": 20537,
"ump1_id": 5057,
"ump2_id": 6057,
"description": "sens_hi_edge[9]",
"access": "RW"
},
{
"modbus": 58,
"address": 20538,
"ump1_id": 5058,
"ump2_id": 6058,
"description": "sens_hi_edge[10]",
"access": "RW"
},
{
"modbus": 59,
"address": 20539,
"ump1_id": 5059,
"ump2_id": 6059,
"description": "sens_hi_edge[11]",
"access": "RW"
},
{
"modbus": 60,
"address": 20540,
"ump1_id": 5060,
"ump2_id": 6060,
"description": "sens_hi_edge[12]",
"access": "RW"
},
{
"modbus": 61,
"address": 20541,
"ump1_id": 5061,
"ump2_id": 6061,
"description": "sens_hi_edge[13]",
"access": "RW"
},
{
"modbus": 62,
"address": 20542,
"ump1_id": 5062,
"ump2_id": 6062,
"description": "sens_hi_edge[14]",
"access": "RW"
},
{
"modbus": 63,
"address": 20543,
"ump1_id": 5063,
"ump2_id": 6063,
"description": "sens_hi_edge[15]",
"access": "RW"
},
{
"modbus": 64,
"address": 20544,
"ump1_id": 5064,
"ump2_id": 6064,
"description": "sens_hi_edge[16]",
"access": "RW"
},
{
"modbus": 65,
"address": 20545,
"ump1_id": 5065,
"ump2_id": 6065,
"description": "sens_hi_edge[17]",
"access": "RW"
},
{
"modbus": 66,
"address": 20546,
"ump1_id": 5066,
"ump2_id": 6066,
"description": "sens_hi_edge[18]",
"access": "RW"
},
{
"modbus": 67,
"address": 20547,
"ump1_id": 5067,
"ump2_id": 6067,
"description": "sens_hi_edge[19]",
"access": "RW"
},
{
"modbus": 68,
"address": 20548,
"ump1_id": 5068,
"ump2_id": 6068,
"description": "sens_hi_edge[20]",
"access": "RW"
},
{
"modbus": 69,
"address": 20549,
"ump1_id": 5069,
"ump2_id": 6069,
"description": "sens_hi_edge[21]",
"access": "RW"
},
{
"modbus": 70,
"address": 20550,
"ump1_id": 5070,
"ump2_id": 6070,
"description": "sens_hi_edge[22]",
"access": "RW"
},
{
"modbus": 71,
"address": 20551,
"ump1_id": 5071,
"ump2_id": 6071,
"description": "sens_hi_edge[23]",
"access": "RW"
},
{
"modbus": 72,
"address": 20552,
"ump1_id": 5072,
"ump2_id": 6072,
"description": "sens_lo_edge[0]",
"access": "RW"
},
{
"modbus": 73,
"address": 20553,
"ump1_id": 5073,
"ump2_id": 6073,
"description": "sens_lo_edge[1]",
"access": "RW"
},
{
"modbus": 74,
"address": 20554,
"ump1_id": 5074,
"ump2_id": 6074,
"description": "sens_lo_edge[2]",
"access": "RW"
},
{
"modbus": 75,
"address": 20555,
"ump1_id": 5075,
"ump2_id": 6075,
"description": "sens_lo_edge[3]",
"access": "RW"
},
{
"modbus": 76,
"address": 20556,
"ump1_id": 5076,
"ump2_id": 6076,
"description": "sens_lo_edge[4]",
"access": "RW"
},
{
"modbus": 77,
"address": 20557,
"ump1_id": 5077,
"ump2_id": 6077,
"description": "sens_lo_edge[5]",
"access": "RW"
},
{
"modbus": 78,
"address": 20558,
"ump1_id": 5078,
"ump2_id": 6078,
"description": "sens_lo_edge[6]",
"access": "RW"
},
{
"modbus": 79,
"address": 20559,
"ump1_id": 5079,
"ump2_id": 6079,
"description": "sens_lo_edge[7]",
"access": "RW"
},
{
"modbus": 80,
"address": 20560,
"ump1_id": 5080,
"ump2_id": 6080,
"description": "sens_lo_edge[8]; LOAD: DAC_go — параметр начала рампы",
"access": "RW"
},
{
"modbus": 81,
"address": 20561,
"ump1_id": 5081,
"ump2_id": 6081,
"description": "sens_lo_edge[9]; LOAD: DAC_stop — параметр конца рампы",
"access": "RW"
},
{
"modbus": 82,
"address": 20562,
"ump1_id": 5082,
"ump2_id": 6082,
"description": "sens_lo_edge[10]; LOAD: LOAD_CAN_TIMEOUT, мс",
"access": "RW; LOAD: 1…60000"
},
{
"modbus": 83,
"address": 20563,
"ump1_id": 5083,
"ump2_id": 6083,
"description": "sens_lo_edge[11]",
"access": "RW"
},
{
"modbus": 84,
"address": 20564,
"ump1_id": 5084,
"ump2_id": 6084,
"description": "sens_lo_edge[12]",
"access": "RW"
},
{
"modbus": 85,
"address": 20565,
"ump1_id": 5085,
"ump2_id": 6085,
"description": "sens_lo_edge[13]",
"access": "RW"
},
{
"modbus": 86,
"address": 20566,
"ump1_id": 5086,
"ump2_id": 6086,
"description": "sens_lo_edge[14]",
"access": "RW"
},
{
"modbus": 87,
"address": 20567,
"ump1_id": 5087,
"ump2_id": 6087,
"description": "sens_lo_edge[15]",
"access": "RW"
},
{
"modbus": 88,
"address": 20568,
"ump1_id": 5088,
"ump2_id": 6088,
"description": "sens_lo_edge[16]",
"access": "RW"
},
{
"modbus": 89,
"address": 20569,
"ump1_id": 5089,
"ump2_id": 6089,
"description": "sens_lo_edge[17]",
"access": "RW"
},
{
"modbus": 90,
"address": 20570,
"ump1_id": 5090,
"ump2_id": 6090,
"description": "sens_lo_edge[18]",
"access": "RW"
},
{
"modbus": 91,
"address": 20571,
"ump1_id": 5091,
"ump2_id": 6091,
"description": "sens_lo_edge[19]",
"access": "RW"
},
{
"modbus": 92,
"address": 20572,
"ump1_id": 5092,
"ump2_id": 6092,
"description": "sens_lo_edge[20]",
"access": "RW"
},
{
"modbus": 93,
"address": 20573,
"ump1_id": 5093,
"ump2_id": 6093,
"description": "sens_lo_edge[21]",
"access": "RW"
},
{
"modbus": 94,
"address": 20574,
"ump1_id": 5094,
"ump2_id": 6094,
"description": "sens_lo_edge[22]",
"access": "RW"
},
{
"modbus": 95,
"address": 20575,
"ump1_id": 5095,
"ump2_id": 6095,
"description": "sens_lo_edge[23]",
"access": "RW"
},
{
"modbus": 96,
"address": 20576,
"ump1_id": 5096,
"ump2_id": 6096,
"description": "Cancount[m_FAST] — пауза быстрых посылок CAN",
"access": "RW"
},
{
"modbus": 97,
"address": 20577,
"ump1_id": 5097,
"ump2_id": 6097,
"description": "Cancount[m_SLOW] — пауза медленных посылок CAN",
"access": "RW"
},
{
"modbus": 98,
"address": 20578,
"ump1_id": 5098,
"ump2_id": 6098,
"description": "Brightness / Bright[0] — яркость",
"access": "RW"
},
{
"modbus": 99,
"address": 20579,
"ump1_id": 5099,
"ump2_id": 6099,
"description": "Owncount / Bright[1]; LOAD: исходное значение АЦП",
"access": "RW"
},
{
"modbus": 100,
"address": 20580,
"ump1_id": 5100,
"ump2_id": 6100,
"description": "Bright[2]; Current_count: исходное значение канала 0",
"access": "RW"
},
{
"modbus": 101,
"address": 20581,
"ump1_id": 5101,
"ump2_id": 6101,
"description": "Bright[3]; Current_count: исходное значение канала 1",
"access": "RW"
},
{
"modbus": 102,
"address": 20582,
"ump1_id": 5102,
"ump2_id": 6102,
"description": "Bright[4]; Current_count: исходное значение канала 2",
"access": "RW"
},
{
"modbus": 103,
"address": 20583,
"ump1_id": 5103,
"ump2_id": 6103,
"description": "Bright[5]; Current_count: исходное значение канала 3",
"access": "RW"
},
{
"modbus": 104,
"address": 20584,
"ump1_id": 5104,
"ump2_id": 6104,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 105,
"address": 20585,
"ump1_id": 5105,
"ump2_id": 6105,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 106,
"address": 20586,
"ump1_id": 5106,
"ump2_id": 6106,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 107,
"address": 20587,
"ump1_id": 5107,
"ump2_id": 6107,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 108,
"address": 20588,
"ump1_id": 5108,
"ump2_id": 6108,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 109,
"address": 20589,
"ump1_id": 5109,
"ump2_id": 6109,
"description": "lev_count[], запись из Current_count; назначение по режиму",
"access": "RW"
},
{
"modbus": 110,
"address": 20590,
"ump1_id": 5110,
"ump2_id": 6110,
"description": "DAC_20 — калибровка входа 20 мА",
"access": "RW"
},
{
"modbus": 111,
"address": 20591,
"ump1_id": 5111,
"ump2_id": 6111,
"description": "DAC_04 — калибровка входа 4 мА",
"access": "RW"
},
{
"modbus": 112,
"address": 20592,
"ump1_id": 5112,
"ump2_id": 6112,
"description": "Zero_lev[0] — нулевой уровень канала тока 0",
"access": "RW"
},
{
"modbus": 113,
"address": 20593,
"ump1_id": 5113,
"ump2_id": 6113,
"description": "Zero_lev[1] — нулевой уровень канала тока 1",
"access": "RW"
},
{
"modbus": 114,
"address": 20594,
"ump1_id": 5114,
"ump2_id": 6114,
"description": "Zero_lev[2] — нулевой уровень канала тока 2",
"access": "RW"
},
{
"modbus": 115,
"address": 20595,
"ump1_id": 5115,
"ump2_id": 6115,
"description": "Zero_lev[3] — нулевой уровень канала тока 3",
"access": "RW"
},
{
"modbus": 116,
"address": 20596,
"ump1_id": 5116,
"ump2_id": 6116,
"description": "Caliber[0]: K380_1 / KPOW_1",
"access": "RW"
},
{
"modbus": 117,
"address": 20597,
"ump1_id": 5117,
"ump2_id": 6117,
"description": "Caliber[1]: K380_2 / KPOW_2",
"access": "RW"
},
{
"modbus": 118,
"address": 20598,
"ump1_id": 5118,
"ump2_id": 6118,
"description": "Caliber[2]: KPOW_3",
"access": "RW"
},
{
"modbus": 119,
"address": 20599,
"ump1_id": 5119,
"ump2_id": 6119,
"description": "Caliber[3]: KPOW_4",
"access": "RW"
},
{
"modbus": 120,
"address": 20600,
"ump1_id": 5120,
"ump2_id": 6120,
"description": "Caliber[4] / TCaliber[0]; LOAD: DAC_max()",
"access": "RW"
},
{
"modbus": 121,
"address": 20601,
"ump1_id": 5121,
"ump2_id": 6121,
"description": "Caliber[5] / TCaliber[1]; LOAD: DAC_min()",
"access": "RW"
},
{
"modbus": 122,
"address": 20602,
"ump1_id": 5122,
"ump2_id": 6122,
"description": "Caliber[6] / TCaliber[2]; LOAD: DAC_cal()",
"access": "RW"
},
{
"modbus": 123,
"address": 20603,
"ump1_id": 5123,
"ump2_id": 6123,
"description": "Caliber[7] / TCaliber[3]; LOAD: текущий 12-битный код ЦАП",
"access": "RW"
},
{
"modbus": 124,
"address": 20604,
"ump1_id": 5124,
"ump2_id": 6124,
"description": "NormaU = Caliber[8]; LOAD: LOAD_STATUS",
"access": "RO для LOAD; иначе RW"
},
{
"modbus": 125,
"address": 20605,
"ump1_id": 5125,
"ump2_id": 6125,
"description": "DeltaU = Caliber[9]; LOAD: LOAD_ATTEMPTS",
"access": "RO для LOAD; иначе RW"
},
{
"modbus": 126,
"address": 20606,
"ump1_id": 5126,
"ump2_id": 6126,
"description": "LastMode",
"access": "RW"
},
{
"modbus": 127,
"address": 20607,
"ump1_id": 5127,
"ump2_id": 6127,
"description": "Commands — битовая маска команд",
"access": "RW"
}
]
}

View File

@@ -17,8 +17,6 @@ from dataclasses import dataclass, field
from enum import IntEnum
from typing import List, Optional
from .gas_167 import assigned_metadata, describe_word
try:
from .native import get_native_core
_SHARED_PROTOCOL = get_native_core()
@@ -116,7 +114,7 @@ MSGTYPE_RU = {
MsgType.BROADCAST: "Broadcast",
MsgType.DISCRETE: "Discrete",
MsgType.ANALOG: "Analog",
MsgType.GENERAL_ADDRESS_SPACE: "GAS (General Address Space)",
MsgType.GENERAL_ADDRESS_SPACE: "General Address Space",
MsgType.MODBUS_COIL: "Modbus Coil",
MsgType.MODBUS_DISCRETE: "Modbus Discrete",
MsgType.MODBUS_HOLDING: "Modbus Holding",
@@ -624,49 +622,11 @@ def _decode_gas(res: Decoded) -> None:
return
for adr, v in res.registers:
res.fields.append((" Reg 0x%04X" % adr, "0x%04X (%d)" % (v, v)))
_append_gas_metadata(res, adr)
if not vals:
_append_gas_metadata(res, start)
if vals:
res.summary = "GAS: %d рег. с 0x%04X = %s" % (
len(vals), start, " ".join("%04X" % v for v in vals))
else:
res.summary = "GAS: запрос по адресу 0x%04X" % start
# Every monitor, manual decoder and export uses this shared summary.
# Keep names in the summary as well as in the expandable field list.
addresses = res.registers if vals else [(start, None)]
if any(assigned_metadata(address) for address, _value in addresses):
words = []
for address, value in addresses:
entry = assigned_metadata(address)
label = "ID %d (0x%04X)" % (address, address)
if entry:
label = "УМП %d · %s · %s" % (
entry['node'] - 4, label, entry['name'])
else:
label += " · имя не назначено"
if value is not None:
label += " = %d [0x%04X]" % (value, value)
description = describe_word(entry['modbus'], value) if entry else ''
if description:
label += " — " + description
res.fields.append((" GAS ID %d / биты" % address, description))
words.append(label)
res.summary = ("GAS: " if vals else "GAS запрос: ") + "; ".join(words)
def _append_gas_metadata(res: Decoded, address: int) -> None:
entry = assigned_metadata(address)
if not entry:
return
label = " GAS ID %d" % address
res.fields += [
(label, "УМП %d — %s" % (entry['node'] - 4, entry['name'])),
(label + " / статус", entry['status']),
(label + " / Modbus 167/v1", "0x%04X" % entry['modbus']),
(label + " / GAS 167/v1", "0x%04X" % entry['legacy_address']),
(label + " / доступ", entry['access']),
]
def _decode_modbus_bit(res: Decoded) -> None:

View File

@@ -73,7 +73,6 @@ class TmsTarget:
kind: str = "tms"
board: int = 0
load_only: bool = False
verify: bool = True
def validate(self, size: int) -> None:
if not 0 <= self.controller <= 255:
@@ -117,8 +116,6 @@ def programming_steps(data: bytes, target: TmsTarget):
raise ValueError(f"Проверка EEPROM не пройдена: код {error}, обработано {words} слов")
return
yield tflash(t.controller, t.ram, t.flash, len(data)), 6, 180000, "CMD_TFLASH: запись Flash", 85
if not t.verify:
return
for offset in range(0, len(data), 256):
block = data[offset:offset + 256]
count = (len(block) + 1) & ~1

View File

@@ -19,4 +19,4 @@ include = ["firmware_image*", "altera_logic*", "logic_analyzer*", "set_devices*"
[tool.setuptools.package-data]
set_devices = ["gas_logger_maps/*.json"]
protocan = ["*.json", "native/*.dll", "native/*.so", "native/*.dylib"]
protocan = ["native/*.dll", "native/*.so", "native/*.dylib"]

View File

@@ -206,16 +206,10 @@ class RawProtocolWidget(QWidget):
def init_ui(self):
layout = QVBoxLayout()
layout.setProperty("fixedPanelLayout", True)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(4)
# Группа для ПРЯМОГО ДОСТУПА К ПАМЯТИ (PEEK/POKE)
mem_group = QGroupBox("Прямой доступ к памяти (PEEK/POKE)")
mem_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mem_layout = QGridLayout()
mem_layout.setContentsMargins(6, 6, 6, 6)
mem_layout.setSpacing(4)
mem_layout.addWidget(QLabel("Адрес памяти (hex):"), 0, 0)
self.mem_addr_edit = QLineEdit("2000")
@@ -245,9 +239,6 @@ class RawProtocolWidget(QWidget):
# Группа для RAW отправки
raw_group = QGroupBox("RAW отправка/чтение")
raw_layout = QVBoxLayout()
raw_layout.setProperty("fixedPanelLayout", True)
raw_layout.setContentsMargins(6, 6, 6, 6)
raw_layout.setSpacing(4)
self.raw_send_edit = QLineEdit()
self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки")
@@ -255,7 +246,6 @@ class RawProtocolWidget(QWidget):
raw_layout.addWidget(self.raw_send_edit)
hbox = QHBoxLayout()
hbox.setSpacing(4)
self.raw_send_btn = QPushButton("Отправить RAW")
self.raw_send_btn.clicked.connect(self.send_raw_data)
hbox.addWidget(self.raw_send_btn)
@@ -270,19 +260,16 @@ class RawProtocolWidget(QWidget):
self.raw_output = QTextEdit()
self.raw_output.setReadOnly(True)
self.raw_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
raw_layout.addWidget(self.raw_output, 1)
self.raw_output.setMaximumHeight(100)
raw_layout.addWidget(self.raw_output)
self.raw_clear_btn = QPushButton("Очистить логи")
self.raw_clear_btn.clicked.connect(self.clear_logs)
raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft)
for button in (self.mem_write_btn, self.mem_read_btn, self.raw_send_btn,
self.raw_read_btn, self.raw_clear_btn):
button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
raw_group.setLayout(raw_layout)
layout.addWidget(raw_group, 1)
layout.addWidget(raw_group)
self.setLayout(layout)
def memory_write(self):

Some files were not shown because too many files have changed in this diff Show More