Compare commits
4 Commits
main
...
qcustomplo
| Author | SHA1 | Date | |
|---|---|---|---|
| bbb4bc2b9d | |||
| 20454ff7fa | |||
| 3c0f6fd9e6 | |||
| f1a7bcd8bc |
@@ -1,111 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,85 +0,0 @@
|
||||
"""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())
|
||||
@@ -7,13 +7,10 @@ protocol encoding lives here: the generated header contains scalar constants.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
@@ -39,14 +36,12 @@ 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")
|
||||
# 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.write_text(text, encoding="utf-8", newline="\n")
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def prepare(repository: Path, templates: Path, config: Path, output: Path) -> dict:
|
||||
settings = json.loads(config.read_text(encoding="utf-8-sig"))
|
||||
settings = json.loads(config.read_text(encoding="utf-8"))
|
||||
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)))):
|
||||
@@ -107,73 +102,6 @@ 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)
|
||||
@@ -186,14 +114,9 @@ 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, "archive": archive_build}[command](**args)
|
||||
result = {"prepare": prepare, "seal": seal, "verify": verify}[command](**args)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
|
||||
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
|
||||
# запускать их следует через CTest из каталога сборки.
|
||||
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
|
||||
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
|
||||
# запускать их следует через CTest из каталога сборки.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(protocan_boot C)
|
||||
@@ -17,10 +17,31 @@ 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()
|
||||
|
||||
@@ -97,7 +97,8 @@ typedef struct {
|
||||
} pcan_boot_port_t;
|
||||
```
|
||||
|
||||
Обязательны `send`, `erase_slot`, `write_slot` и `set_pending_slot`.
|
||||
Обязательны `send`, `erase_slot`, `write_slot`; в A/B также `set_pending_slot`.
|
||||
В single-slot этот callback необязателен и может завершать активацию образа.
|
||||
`authorize` проверяет политику до стирания (тип, версия, anti-rollback).
|
||||
`verify_image` выполняет платформенную проверку подписанного контейнера после
|
||||
CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки
|
||||
@@ -175,5 +176,12 @@ ctest --test-dir build --output-on-failure
|
||||
- включить watchdog и rollback при неподтверждённом запуске;
|
||||
- согласовать фильтры CAN для `MsgType=0x9..0xD`.
|
||||
|
||||
Проект пока не подключён ни к одной конкретной плате; это самостоятельный
|
||||
шаблон для интеграции в будущие загрузчики приборов SET.
|
||||
Ядро поддерживает платформенные порты; особенности интеграции перечислены ниже.
|
||||
|
||||
## F28335 / большая EEPROM
|
||||
|
||||
[Порт M95M02](ports/tms320f28335/README.md) подключён в `tms_periph/periph_167`.
|
||||
Использует отдельный Flash-загрузчик и два слота EEPROM по 130560 байт.
|
||||
Реализованы readback/CRC, проверка SCI8, журнал метаданных, пробный запуск,
|
||||
CONFIRM/откат, PING/discovery и сервисный паспорт. Первичная установка Flash-
|
||||
загрузчика и выбор Flash boot описаны в README порта.
|
||||
|
||||
@@ -63,6 +63,10 @@ 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`
|
||||
|
||||
@@ -81,7 +85,8 @@ DATA[4..7] FirmwareVersion, uint32 little-endian
|
||||
```
|
||||
|
||||
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
|
||||
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
|
||||
тип изделия и аппаратную ревизию. Проверка версии/anti-rollback выполняется
|
||||
только при наличии соответствующей политики платформы (callback authorize).
|
||||
|
||||
## `BOOT_STATUS`
|
||||
|
||||
@@ -181,3 +186,16 @@ 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.
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
#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
|
||||
@@ -41,7 +48,11 @@ 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_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_command_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -66,7 +77,8 @@ typedef enum {
|
||||
PCAN_BOOT_STATE_READY_TO_ERASE,
|
||||
PCAN_BOOT_STATE_RECEIVING,
|
||||
PCAN_BOOT_STATE_VERIFIED,
|
||||
PCAN_BOOT_STATE_FAILED
|
||||
PCAN_BOOT_STATE_FAILED,
|
||||
PCAN_BOOT_STATE_COMMITTED
|
||||
} pcan_boot_state_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -74,36 +86,47 @@ typedef struct {
|
||||
uint32_t image_crc32;
|
||||
uint32_t firmware_version;
|
||||
uint16_t product_type;
|
||||
uint8_t hardware_revision_min;
|
||||
uint8_t hardware_revision_max;
|
||||
pcan_boot_byte_t hardware_revision_min;
|
||||
pcan_boot_byte_t hardware_revision_max;
|
||||
} pcan_boot_manifest_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t device_type;
|
||||
uint8_t device_id;
|
||||
pcan_boot_byte_t device_type;
|
||||
pcan_boot_byte_t device_id;
|
||||
uint16_t product_type;
|
||||
uint8_t hardware_revision;
|
||||
pcan_boot_byte_t hardware_revision;
|
||||
uint32_t firmware_version;
|
||||
uint8_t active_slot;
|
||||
pcan_boot_byte_t active_slot;
|
||||
/** 1 for an in-place/single-slot target, 2 for A/B. Zero keeps legacy A/B. */
|
||||
uint8_t slot_count;
|
||||
pcan_boot_byte_t slot_count;
|
||||
/** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */
|
||||
uint32_t max_image_size;
|
||||
uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
|
||||
pcan_boot_byte_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
|
||||
uint32_t session_timeout_ms; /**< 0 disables timeout for legacy ports. */
|
||||
} pcan_boot_config_t;
|
||||
|
||||
typedef struct {
|
||||
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 (*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 (*authorize)(void *user, const pcan_boot_manifest_t *manifest);
|
||||
bool (*verify_image)(void *user, uint8_t slot,
|
||||
bool (*verify_image)(void *user, pcan_boot_byte_t slot,
|
||||
const pcan_boot_manifest_t *manifest);
|
||||
bool (*set_pending_slot)(void *user, uint8_t slot,
|
||||
/** A/B metadata, or optional single-slot activation after verification. */
|
||||
bool (*set_pending_slot)(void *user, pcan_boot_byte_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 {
|
||||
@@ -115,11 +138,13 @@ typedef struct {
|
||||
uint32_t running_crc32;
|
||||
uint32_t bytes_received;
|
||||
uint16_t next_block;
|
||||
uint8_t session_id;
|
||||
uint8_t target_slot;
|
||||
uint8_t last_status;
|
||||
pcan_boot_byte_t session_id;
|
||||
pcan_boot_byte_t target_slot;
|
||||
pcan_boot_byte_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 должны быть заданы. */
|
||||
@@ -134,16 +159,19 @@ void pcan_boot_abort(pcan_boot_t *boot);
|
||||
* Возвращает true, если кадр принадлежал загрузочному сервису этого прибора.
|
||||
*/
|
||||
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
|
||||
const uint8_t *data, uint8_t dlc);
|
||||
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);
|
||||
|
||||
/** Переносимые операции с 29-битным ProtoCAN 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);
|
||||
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);
|
||||
uint16_t pcan_boot_id_msg_body(uint32_t can_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
156
c/protocan-boot/ports/tms320f28335/README.md
Normal file
156
c/protocan-boot/ports/tms320f28335/README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# 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 выполняет этот загрузчик.
|
||||
181
c/protocan-boot/ports/tms320f28335/f28335_boot_hw.c
Normal file
181
c/protocan-boot/ports/tms320f28335/f28335_boot_hw.c
Normal file
@@ -0,0 +1,181 @@
|
||||
/* 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;
|
||||
}
|
||||
17
c/protocan-boot/ports/tms320f28335/f28335_boot_platform.h
Normal file
17
c/protocan-boot/ports/tms320f28335/f28335_boot_platform.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#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
|
||||
254
c/protocan-boot/ports/tms320f28335/f28335_boot_store.c
Normal file
254
c/protocan-boot/ports/tms320f28335/f28335_boot_store.c
Normal file
@@ -0,0 +1,254 @@
|
||||
/* Хранилище 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);
|
||||
}
|
||||
55
c/protocan-boot/ports/tms320f28335/f28335_boot_store.h
Normal file
55
c/protocan-boot/ports/tms320f28335/f28335_boot_store.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#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
|
||||
276
c/protocan-boot/ports/tms320f28335/f28335_can_boot.c
Normal file
276
c/protocan-boot/ports/tms320f28335/f28335_can_boot.c
Normal file
@@ -0,0 +1,276 @@
|
||||
/* 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
|
||||
28
c/protocan-boot/ports/tms320f28335/f28335_can_boot.h
Normal file
28
c/protocan-boot/ports/tms320f28335/f28335_can_boot.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#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
|
||||
@@ -1,21 +1,16 @@
|
||||
/*
|
||||
* Автомат адресной загрузки прошивки по ProtoCAN с сессией, контрольной суммой и A/B-слотами.
|
||||
* Приём блоков, проверка образа и активация разделены на этапы; операции Flash и метаданные
|
||||
* слотов предоставляет платформа.
|
||||
*/
|
||||
|
||||
#include "pcan_boot.h"
|
||||
/* ProtoCAN update state machine; platform owns storage and reset. */
|
||||
#include "../include/pcan_boot.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL
|
||||
|
||||
static uint16_t get_u16(const uint8_t *p)
|
||||
static uint16_t get_u16(const pcan_boot_byte_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t get_u32(const uint8_t *p)
|
||||
static uint32_t get_u32(const pcan_boot_byte_t *p)
|
||||
{
|
||||
return (uint32_t)p[0]
|
||||
| ((uint32_t)p[1] << 8)
|
||||
@@ -23,26 +18,26 @@ static uint32_t get_u32(const uint8_t *p)
|
||||
| ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static void put_u16(uint8_t *p, uint16_t value)
|
||||
static void put_u16(pcan_boot_byte_t *p, uint16_t value)
|
||||
{
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
|
||||
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
|
||||
}
|
||||
|
||||
static void put_u32(uint8_t *p, uint32_t value)
|
||||
static void put_u32(pcan_boot_byte_t *p, uint32_t value)
|
||||
{
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
p[2] = (uint8_t)(value >> 16);
|
||||
p[3] = (uint8_t)(value >> 24);
|
||||
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);
|
||||
}
|
||||
|
||||
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
|
||||
static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t length)
|
||||
{
|
||||
size_t i;
|
||||
crc = ~crc;
|
||||
for (i = 0U; i < length; ++i) {
|
||||
uint8_t bit;
|
||||
pcan_boot_byte_t bit;
|
||||
crc ^= data[i];
|
||||
for (bit = 0U; bit < 8U; ++bit) {
|
||||
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U);
|
||||
@@ -51,9 +46,9 @@ static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
{
|
||||
return ((((uint32_t)priority & 1U) << 28)
|
||||
| (((uint32_t)route & 1U) << 27)
|
||||
@@ -63,14 +58,14 @@ uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
|
||||
| msg_body) & PCAN_BOOT_ID_MASK;
|
||||
}
|
||||
|
||||
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); }
|
||||
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); }
|
||||
uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; }
|
||||
|
||||
static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
|
||||
const uint8_t *data, uint8_t dlc)
|
||||
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)
|
||||
{
|
||||
uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE,
|
||||
boot->config.device_type,
|
||||
@@ -79,9 +74,12 @@ static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
|
||||
return boot->port.send(boot->port_user, id, data, dlc);
|
||||
}
|
||||
|
||||
static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
|
||||
/* 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)
|
||||
{
|
||||
uint8_t data[8];
|
||||
pcan_boot_byte_t data[8];
|
||||
uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command);
|
||||
data[0] = status;
|
||||
data[1] = boot->target_slot;
|
||||
@@ -91,9 +89,11 @@ static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
|
||||
return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U);
|
||||
}
|
||||
|
||||
/* IDENTIFY имеет отдельный тип сообщения DISCOVERY и Body=1. Он доступен
|
||||
* без открытия сессии, поэтому обнаружение устройства не должно запускать обновление. */
|
||||
static bool send_identity(pcan_boot_t *boot)
|
||||
{
|
||||
uint8_t data[8];
|
||||
pcan_boot_byte_t data[8];
|
||||
put_u16(&data[0], boot->config.product_type);
|
||||
data[2] = boot->config.hardware_revision;
|
||||
data[3] = PCAN_BOOT_VERSION;
|
||||
@@ -101,6 +101,9 @@ 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) {
|
||||
@@ -118,19 +121,24 @@ 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)
|
||||
{
|
||||
uint8_t slot_count;
|
||||
pcan_boot_byte_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 > 1U) && (config->active_slot != PCAN_BOOT_SLOT_NONE))
|
||||
|| (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)
|
||||
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
|
||||
|| ((slot_count == 2U) && (port->set_pending_slot == NULL))) {
|
||||
return false;
|
||||
}
|
||||
@@ -149,7 +157,10 @@ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
|
||||
/* 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)
|
||||
{
|
||||
if (!boot->have_image || !boot->have_compat) {
|
||||
boot->state = PCAN_BOOT_STATE_METADATA;
|
||||
@@ -160,8 +171,11 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_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->config.hardware_revision < boot->manifest.hardware_revision_min)
|
||||
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)
|
||||
|| (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);
|
||||
@@ -171,19 +185,51 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
|
||||
boot->state = PCAN_BOOT_STATE_FAILED;
|
||||
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR);
|
||||
}
|
||||
boot->target_slot = boot->config.slot_count == 1U
|
||||
boot->target_slot = boot->config.active_slot == PCAN_BOOT_SLOT_NONE ? 0U :
|
||||
boot->config.slot_count == 1U
|
||||
? boot->config.active_slot
|
||||
: (uint8_t)(boot->config.active_slot ^ 1U);
|
||||
: (pcan_boot_byte_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 uint8_t *data, uint8_t dlc)
|
||||
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
|
||||
{
|
||||
uint8_t session = (uint8_t)(body >> 8);
|
||||
uint8_t command = (uint8_t)body;
|
||||
pcan_boot_byte_t session = (pcan_boot_byte_t)(body >> 8);
|
||||
pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -191,8 +237,18 @@ 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);
|
||||
}
|
||||
@@ -200,8 +256,11 @@ 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);
|
||||
}
|
||||
@@ -211,6 +270,9 @@ 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);
|
||||
}
|
||||
@@ -222,6 +284,8 @@ 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);
|
||||
}
|
||||
@@ -236,6 +300,8 @@ 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);
|
||||
@@ -244,7 +310,13 @@ 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_image != NULL)
|
||||
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)
|
||||
&& !boot->port.verify_image(boot->port_user, boot->target_slot,
|
||||
&boot->manifest)) {
|
||||
boot->state = PCAN_BOOT_STATE_FAILED;
|
||||
@@ -254,21 +326,16 @@ 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->config.slot_count == 2U)
|
||||
if ((boot->port.set_pending_slot != NULL)
|
||||
&& !boot->port.set_pending_slot(boot->port_user, boot->target_slot,
|
||||
&boot->manifest)) {
|
||||
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
|
||||
}
|
||||
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);
|
||||
}
|
||||
boot->state=PCAN_BOOT_STATE_COMMITTED;
|
||||
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
|
||||
|
||||
case PCAN_BOOT_CMD_QUERY_PROGRESS:
|
||||
@@ -280,7 +347,9 @@ 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:
|
||||
(void)send_status(boot, command, PCAN_BOOT_STATUS_OK);
|
||||
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;
|
||||
if (boot->port.reboot != NULL) {
|
||||
boot->port.reboot(boot->port_user);
|
||||
}
|
||||
@@ -291,11 +360,16 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
|
||||
}
|
||||
}
|
||||
|
||||
static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
|
||||
const uint8_t *data, uint8_t dlc)
|
||||
/* 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)
|
||||
{
|
||||
uint8_t expected_type;
|
||||
uint8_t write_length;
|
||||
pcan_boot_byte_t expected_type;
|
||||
pcan_boot_byte_t write_length;
|
||||
uint32_t remaining;
|
||||
|
||||
if (boot->state != PCAN_BOOT_STATE_RECEIVING) {
|
||||
@@ -309,8 +383,9 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
|
||||
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 ? (uint8_t)remaining
|
||||
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (pcan_boot_byte_t)remaining
|
||||
: PCAN_BOOT_BLOCK_SIZE;
|
||||
if ((write_length == 0U)
|
||||
|| !boot->port.write_slot(boot->port_user, boot->target_slot,
|
||||
@@ -329,10 +404,14 @@ static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Общая точка входа для одного кадра после аппаратной фильтрации.
|
||||
* Проверки адреса повторяются программно, чтобы широкая маска CAN не разрешила
|
||||
* обновление чужого устройства. Broadcast допускается только для DISCOVERY
|
||||
* с Body=0/DLC=0; ответ откладывается до poll для разведения устройств по времени. */
|
||||
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
|
||||
const uint8_t *data, uint8_t dlc)
|
||||
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
|
||||
{
|
||||
uint8_t msg_type;
|
||||
pcan_boot_byte_t msg_type;
|
||||
if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) {
|
||||
return false;
|
||||
}
|
||||
@@ -340,6 +419,17 @@ 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)) {
|
||||
@@ -353,3 +443,18 @@ 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;
|
||||
}
|
||||
|
||||
13
c/protocan-boot/tests/f28335_stubs/DSP2833x_Device.h
Normal file
13
c/protocan-boot/tests/f28335_stubs/DSP2833x_Device.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#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
|
||||
7
c/protocan-boot/tests/f28335_stubs/spise2p.h
Normal file
7
c/protocan-boot/tests/f28335_stubs/spise2p.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#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);
|
||||
27
c/protocan-boot/tests/run_tests.py
Normal file
27
c/protocan-boot/tests/run_tests.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""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)
|
||||
118
c/protocan-boot/tests/test_f28335_boot_store.c
Normal file
118
c/protocan-boot/tests/test_f28335_boot_store.c
Normal file
@@ -0,0 +1,118 @@
|
||||
#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;
|
||||
}
|
||||
94
c/protocan-boot/tests/test_f28335_can_boot.c
Normal file
94
c/protocan-boot/tests/test_f28335_can_boot.c
Normal file
@@ -0,0 +1,94 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -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_HARDWARE);
|
||||
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_DEVICE);
|
||||
assert(boot.state == PCAN_BOOT_STATE_FAILED);
|
||||
}
|
||||
|
||||
@@ -306,8 +306,41 @@ 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();
|
||||
|
||||
@@ -31,7 +31,8 @@ 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..128, with focus anchoring.
|
||||
* unchanged viewport. Zoom is restricted to 1..10000000 on X and 1..128 on Y,
|
||||
* 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,
|
||||
|
||||
@@ -13,6 +13,7 @@ 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).
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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 "Передача выключена")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
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("Отмена") } })
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
#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;
|
||||
@@ -23,12 +26,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] < 1.0/128 || v[2] > 1 ||
|
||||
if (!finite_values(v, 4) || v[2] < MIN_VIEW_WIDTH || 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], 1.0/128, 1);
|
||||
w = clamp(v[2] / v[4], MIN_VIEW_WIDTH, 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);
|
||||
|
||||
@@ -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>10000||work_count<14*count+128
|
||||
||output_count<2||output_count>1000000||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;
|
||||
|
||||
722
c/set-protocol/tests/fixtures/deif-setgui.json
vendored
722
c/set-protocol/tests/fixtures/deif-setgui.json
vendored
@@ -1,722 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
2
c/set-protocol/tests/fixtures/plot-v1.json
vendored
2
c/set-protocol/tests/fixtures/plot-v1.json
vendored
@@ -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.49609375,0.49609375,0.0078125,0.0078125]},
|
||||
{"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":"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]},
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 и привязка датчиков |
|
||||
|
||||
97
doc/setcan/protocan/GAS_ADDRESS_ALLOCATION_167.md
Normal file
97
doc/setcan/protocan/GAS_ADDRESS_ALLOCATION_167.md
Normal file
File diff suppressed because one or more lines are too long
288
doc/setcan/protocan/GAS_MODBUS_MAP_167.md
Normal file
288
doc/setcan/protocan/GAS_MODBUS_MAP_167.md
Normal file
File diff suppressed because one or more lines are too long
@@ -112,3 +112,78 @@ 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-снифер; генератор работает локально без передачи в оборудование.
|
||||
|
||||
@@ -597,11 +597,11 @@ CRC32 обнаруживает случайное повреждение, но
|
||||
<td class="x14"> </td>
|
||||
<td colspan="8" class="x96">64 битовых адресса</td>
|
||||
</tr>
|
||||
<tr style="height:17px">
|
||||
<tr style="height:72px">
|
||||
<td class="x26"> </td>
|
||||
<td class="x27"> </td>
|
||||
<td colspan="3" class="x131">0x3</td>
|
||||
<td colspan="4" class="x136">0x0 - 0xF устройств</td>
|
||||
<td colspan="4" class="x136">0x0 - 0xF устройств<br>0x7: F28335-M95M02<br>ProductType: 0x2833</td>
|
||||
<td colspan="12" class="x138">0x3. Установка времени</td>
|
||||
<td colspan="4" class="x140"> </td>
|
||||
<td class="x14"> </td>
|
||||
|
||||
Binary file not shown.
@@ -1,13 +0,0 @@
|
||||
# Перенос контроля версии прошивки
|
||||
|
||||
В 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-файлы сами по себе эту адаптацию не выполняют.
|
||||
@@ -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 not self.from_device and item.address == 0x7F else "")
|
||||
if item.address == 0x7F else "")
|
||||
for item in self.registers
|
||||
) or "нет отмеченных регистров"
|
||||
return "BALZAM · %s · %s · %s" % (self.route, direction, values)
|
||||
|
||||
@@ -37,6 +37,8 @@ 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
|
||||
@@ -46,6 +48,10 @@ 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))
|
||||
@@ -128,4 +134,93 @@ 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
|
||||
|
||||
51
python/protocan/gas_167.py
Normal file
51
python/protocan/gas_167.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""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 ещё не обслуживает'}
|
||||
1030
python/protocan/gas_167_registers.json
Normal file
1030
python/protocan/gas_167_registers.json
Normal file
@@ -0,0 +1,1030 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,6 +17,8 @@ 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()
|
||||
@@ -114,7 +116,7 @@ MSGTYPE_RU = {
|
||||
MsgType.BROADCAST: "Broadcast",
|
||||
MsgType.DISCRETE: "Discrete",
|
||||
MsgType.ANALOG: "Analog",
|
||||
MsgType.GENERAL_ADDRESS_SPACE: "General Address Space",
|
||||
MsgType.GENERAL_ADDRESS_SPACE: "GAS (General Address Space)",
|
||||
MsgType.MODBUS_COIL: "Modbus Coil",
|
||||
MsgType.MODBUS_DISCRETE: "Modbus Discrete",
|
||||
MsgType.MODBUS_HOLDING: "Modbus Holding",
|
||||
@@ -622,11 +624,49 @@ 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:
|
||||
|
||||
@@ -73,6 +73,7 @@ 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:
|
||||
@@ -116,6 +117,8 @@ 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
|
||||
|
||||
@@ -19,4 +19,4 @@ include = ["firmware_image*", "altera_logic*", "logic_analyzer*", "set_devices*"
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
set_devices = ["gas_logger_maps/*.json"]
|
||||
protocan = ["native/*.dll", "native/*.so", "native/*.dylib"]
|
||||
protocan = ["*.json", "native/*.dll", "native/*.so", "native/*.dylib"]
|
||||
|
||||
@@ -206,10 +206,16 @@ 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")
|
||||
@@ -239,6 +245,9 @@ 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 данные для отправки")
|
||||
@@ -246,6 +255,7 @@ 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)
|
||||
@@ -260,16 +270,19 @@ class RawProtocolWidget(QWidget):
|
||||
|
||||
self.raw_output = QTextEdit()
|
||||
self.raw_output.setReadOnly(True)
|
||||
self.raw_output.setMaximumHeight(100)
|
||||
raw_layout.addWidget(self.raw_output)
|
||||
self.raw_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
raw_layout.addWidget(self.raw_output, 1)
|
||||
|
||||
|
||||
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)
|
||||
layout.addWidget(raw_group, 1)
|
||||
|
||||
self.setLayout(layout)
|
||||
def memory_write(self):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user