Appearance
Связи с драйверами
Связь объекта с драйвером: по какому полю и в каком направлении объект обменивается данными с шиной. Одна сущность редактируется с двух сторон: вкладка «Драйверы» объекта (widgets/object-driver) и вкладка «Объекты» драйвера (widgets/driver-objects). Драйвер-специфика — дискриминированный union по driverType: пара (driverType, settings) проверяется компилятором.
Одна и та же связь видна с двух сторон: вкладка «Драйверы» у объекта перечисляет его связи с шинами, вкладка «Объекты» у драйвера — все объекты этого драйвера (общий порядок — поле position).
Контракт связи
ts
import { EObjectType } from "@bms-builder/bms-libui/types";
import { type ICase } from "../../object-reaction/types";
import { EDriverSettingsType } from "./driver-settings-type";
/* eslint-disable import/no-cycle -- type-only ссылки на пер-драйверные контракты */
import type { IReadBACNETDriverSettings } from "../../object/drivers/BACNET/types";
import type { IKNXDriverSettingProps } from "../../object/drivers/KNXIP/types";
import type { IOPCDriverSettingProps } from "../../object/drivers/OPC/types";
import type { ISNMPSettingsProps } from "../../object/drivers/SNMP/types";
import type { IPINGDriverSettingProps } from "../../object/drivers/PING/types";
import type { IMODBUSDriverSettingsProps } from "../../object/drivers/MODBUS/types";
import type { IMQTTDriverSettingsProps } from "../../object/drivers/MQTT/types";
import type { IHTTPDriverSettingProps } from "../../object/drivers/HTTP/types";
import type { TDriverType } from "../../object/drivers/types";
import type { ConcreteObjectInstance } from "../../object/types/concrete-instance";
import type {
IHasDriverLinks,
IObjectReactable,
} from "../../object/types/object-interfaces";
/**
* Объект, владеющий драйвер-линками: reactable-тип с контрактами линков.
* Пересечение отсекает драйвер-объекты (у них линков нет) из общего union.
*/
export type TDriverLinkedObject = ConcreteObjectInstance &
IHasDriverLinks &
IObjectReactable;
export { EDriverSettingsType };
/**
* @description
* Связь объекта с драйвером: по какому полю и в каком направлении объект
* обменивается данными с шиной. Одна сущность редактируется с двух сторон:
* вкладка «Драйверы» объекта (widgets/object-driver) и вкладка «Объекты»
* драйвера (widgets/driver-objects). Драйвер-специфика — дискриминированный
* union по driverType: пара (driverType, settings) проверяется компилятором.
*
* @doc_id driver-links.summary
*/
// #region driver-link-contract-doc
/** Тип связи: чтение или запись данных. */
export enum EDriverLinkType {
READ = "READ",
WRITE = "WRITE",
}
// #region driver-link-field-declarations-doc
/**
* Декларация одного поля драйвер-линка у объекта-владельца поля: лейбл для
* дропдауна и направления обмена с шиной. Знание о направлениях живёт у
* типа-владельца (schema.ts), способность драйвера — у драйвера
* (DRIVER_LINK_CAPABILITY, см. drivers/model/driver-link-capabilities).
*/
export type TDriverLinkFieldDeclaration = {
readonly label: string;
readonly directions: readonly EDriverLinkType[];
};
/** Поля объекта, связываемые драйвер-линками: поле → декларация. */
export type TDriverLinkFieldDeclarations = Readonly<
Record<string, TDriverLinkFieldDeclaration>
>;
/** Способность драйвера в драйвер-линках: readOnly срезает WRITE у всех полей. */
export type TDriverLinkCapability = {
readonly readOnly: boolean;
};
// #endregion driver-link-field-declarations-doc
// #region driver-link-settings-declarations-doc
/**
* Декларация одной настройки связи: лейбл и доступы, при которых настройка
* существует — показывается в редакторе связи и/или уезжает в запрос
* контроллеру (пересечение осей «виден в UI» и «уходит в wire»).
* Состав и доступы живут у драйвера (schema.ts, DRIVER_LINK_SETTINGS).
*/
export type TDriverLinkSettingDeclaration = {
readonly label: string;
readonly accesses: readonly EDriverLinkType[];
};
/**
* Настройки связи драйвер-линка: ключ EDriverSettingsType → декларация.
* Инвариант хранения не меняется — в БД настройки всегда полные, декларация
* фильтрует только показ (settings.vue) и wire-состав (мапперы entity.ts).
*/
export type TDriverLinkSettingsDeclarations = Readonly<
Record<string, TDriverLinkSettingDeclaration>
>;
// #endregion driver-link-settings-declarations-doc
export interface IDriverKatexModificationObjectsData {
id: string;
/** объект, чьё поле подставляется в формулу */
objectId: string;
/** поле объекта в формуле */
field: string;
}
/** Формула KaTeX пересчёта значения связи и её входные данные. */
export interface IDriverKatexModification {
/** формула в синтаксисе KaTeX */
katex: string;
/** входы формулы — поля связанных объектов */
data: IDriverKatexModificationObjectsData[];
}
/**
* Настройки связи в разрезе полей драйвера (BIT/REGISTER/ADDRESS/...).
* Значения разнотипны и зависят от драйвера: строки-числа (MODBUS/SNMP),
* булевы (MQTT), массивы объектов (HTTP HEADERS), а BACNET кладёт и ключи
* вне enum (PLC_NAME). Единый скалярный тип невозможен без пер-драйверных
* контрактов настроек линка; any здесь — честная фиксация текущего зоопарка.
* Совместимость: generic-места, которым нужен произвольный словарь настроек
* (рендер полей по декларациям, wire-границы до их типизации).
*/
export type TDriverSettings = Partial<Record<EDriverSettingsType, any | undefined>>;
/**
* Настройки связи — словарь полей конкретного драйвера. Контракты живут в
* drivers/<тип>/types (свои enum-ключи и типы значений); выбор ветки union —
* по driverType линка. Partial: поля появляются по мере заполнения формы.
*/
export type TDriverLinkSettings =
| Partial<IReadBACNETDriverSettings>
| Partial<IKNXDriverSettingProps>
| Partial<IOPCDriverSettingProps>
| Partial<ISNMPSettingsProps>
| Partial<IPINGDriverSettingProps>
| Partial<IMODBUSDriverSettingsProps>
| Partial<IMQTTDriverSettingsProps>
| Partial<IHTTPDriverSettingProps>;
/**
* Драйвер-специфичная часть линка: дискриминированный union по типу драйвера —
* компилятор проверяет пару (driverType, settings) на согласованность.
*/
export type TDriverSpecificLink =
| { driverType: EObjectType.BACNET; settings?: Partial<IReadBACNETDriverSettings> }
| { driverType: EObjectType.KNXIP; settings?: Partial<IKNXDriverSettingProps> }
| { driverType: EObjectType.OPC; settings?: Partial<IOPCDriverSettingProps> }
| { driverType: EObjectType.SNMP; settings?: Partial<ISNMPSettingsProps> }
| { driverType: EObjectType.PING; settings?: Partial<IPINGDriverSettingProps> }
| { driverType: EObjectType.MODBUS; settings?: Partial<IMODBUSDriverSettingsProps> }
| { driverType: EObjectType.MQTT; settings?: Partial<IMQTTDriverSettingsProps> }
| { driverType: EObjectType.HTTP; settings?: Partial<IHTTPDriverSettingProps> };
/**
* Черновик линка до выбора драйвера/объекта: драйвер-специфика
* (type/driverType/settings) появится при инициализации. В БД не попадает —
* валидация требует непустой objectId.
*/
export interface IDriverLinkDraft {
id: string;
objectId?: string;
modification?: IDriverKatexModification;
position?: number;
logic: ICase<string>[];
/** драйвер-специфика в черновике отсутствует до инициализации */
type?: undefined;
driverType?: undefined;
settings?: undefined;
field?: undefined;
}
export type IDriverLink = {
id: string;
/** id драйвера; undefined, пока драйвер не выбран */
objectId?: string;
/** поле объекта, привязанное к шине */
field?: string;
type: EDriverLinkType;
/** формула пересчёта значения и её входы */
modification?: IDriverKatexModification;
/** позиция в списке объектов драйвера; пересчитывается при drag-and-drop */
position?: number;
/** условия (кейсы) срабатывания связи */
logic?: ICase<string>[];
} & TDriverSpecificLink;
/**
* Не-null форма линка для типизации ошибок валидации: optional-поля IDriverLink
* дают string-ветку в рекурсивном ValidationErrors. Поля — те, чьи ошибки
* читает редактор связи (объект, настройки, кейсы).
*/
export interface IDriverLinkErrorsShape {
objectId: string;
settings: TDriverLinkSettings;
logic: ICase<string>[];
}
// #endregion
export enum ERegisterType {
Binary = "Binary",
RegisterBinary = "RegisterBinary",
Coil = "Coil",
Input = "Input",
Signed = "Signed",
Unsigned = "Unsigned",
Double = "Double",
DoubleInverse = "Double inverse",
Float = "Float",
FloatInverse = "Float inverse",
Long = "Long",
LongInverse = "Long inverse",
}
/**
* Ветка (driverType, settings) для линка от драйвера: драйвер-объект → готовая
* драйвер-часть линка с его дефолтными настройками. Вызывается при создании
* или смене драйвера в линке (редактор связи DriverLinkItem, вкладка «Объекты»
* драйвера — widgets/driver-objects) и спредом укладывается в линк/патч.
*
* Приведение (as): objectType и driverSettings() берутся у одного driver, но
* TS выводит их независимо (объединение всех типов драйверов × unknown) и не
* связывает их в одну ветку TDriverSpecificLink. Гарантия «пара из одного
* драйвера» фиксируется здесь — единственная точка приведения, все места
* вызова остаются без кастов.
*/
export const driverLinkBranch = <
T extends { objectType: TDriverType; driverSettings(): unknown },
>(
driver: T,
): Pick<IDriverLink, "driverType" | "settings"> =>
({
driverType: driver.objectType,
settings: driver.driverSettings(),
}) as Pick<IDriverLink, "driverType" | "settings">;
/**
* Дельта одной правки линка: набор полей, изменённых одним действием
* пользователя. Редактор связи не владеет линком — получает его в props и
* отправляет наверх «текущий линк + патч» через Object.assign
* (updateDriverLink в DriverLinkItem). Правки идут и по черновику
* IDriverLinkDraft (драйвер ещё не выбран), поэтому патч не требует полного
* линка; значение undefined осмысленно — стирает поле или драйвер-ветку,
* возвращая линк к черновику (см. clearDriverLink).
*
* Не Partial<IDriverLink>: Partial по union распределяется на ветки, и даже
* патч одного поля обязан был бы нести согласованную пару driverType↔settings.
* Разрез Omit/Pick делает базовые поля и драйвер-ветку независимыми — менять
* можно что угодно по отдельности; согласованность пары проверяется только в
* целом линке: типом IDriverLink при сборке и zod-схемой при сохранении.
*/
export type TDriverLinkPatch = Partial<Omit<IDriverLink, "driverType" | "settings">> &
Partial<Pick<IDriverLink, "driverType" | "settings">>;
/** Финальный (инициализированный) линк — у черновика driverType отсутствует. */
export const isDriverLinkFinal = (
link: IDriverLink | IDriverLinkDraft,
): link is IDriverLink => !!link.driverType;
/**
* Пересборка линка с патчем. Object.assign на union не распределяет ветки,
* поэтому дискриминация собрана здесь: финальный линк остаётся финальным,
* черновик с патчем driverType становится линком, без него — остаётся
* черновиком. Единственное место слоя с осознанным приведением.
*/
export const applyDriverLinkPatch = (
link: IDriverLink | IDriverLinkDraft,
patch: TDriverLinkPatch,
): IDriverLink | IDriverLinkDraft =>
isDriverLinkFinal(link) || patch.driverType
? (Object.assign({}, link, patch) as IDriverLink)
: (Object.assign({}, link, patch) as IDriverLinkDraft);
/**
* Обогащает конкретный экземпляр до хоста связей: контракт TDriverLinkedObject
* требует массив driverLinks и реактивные члены, в хранилище поле опционально.
* Приведение вместо каста у каждого потребителя карточки связи: рантайм-объект
* методами класса уже обладает, меняется только гарантия типа.
*/
export const toDriverLinkedObject = (
object: ConcreteObjectInstance,
): TDriverLinkedObject => {
object.driverLinks ??= [];
return object as TDriverLinkedObject;
};Ключи настроек
Настройки связи типизированы пер-драйверными контрактами (union по driverType, см. контракт выше); общий реестр ключей EDriverSettingsType — совместимостный слой для generic-рендера и wire-границ. Какие ключи нужны конкретному драйверу — см. Интеграции.
ts
// #region driver-settings-doc
/**
* Ключи настроек связей — общий реестр на все типы драйверов: какой ключ
* нужен конкретному драйверу, определяют его настройки (страницы драйверов).
* Значения полей задаются редактором связи и уходят на контроллер как есть.
*/
export enum EDriverSettingsType {
BIT = "BIT",
TEXTAREA = "TEXTAREA",
TIMEOUT_SEC = "TIMEOUT_SEC",
INTERVAL = "INTERVAL",
ADDRESS = "ADDRESS",
/**
* @description
* Уникальный идентификатор PLC для однозначной связи driverLink с конкретным PLC.
* Используется в BACnet драйвере для надежной привязки связей к PLC, даже когда IP-адрес или порт изменяются.
* Позволяет избежать неоднозначности при одинаковых IP+Port комбинациях и обеспечивает стабильную связь
* между driverLinks и PLC независимо от изменений сетевых параметров.
*
* **Важно:** Это поле используется только на фронтенде для управления связями между driverLinks и PLC.
* При отправке запросов на контроллер (`baseToControllerDriverLinkRequest`) это поле игнорируется,
* на контроллер передаются только ADDRESS и PORT из настроек PLC.
*
* @see {@link apps/builder/src/entities/project/object/drivers/BACNET/ui/BACNET.vue} для логики синхронизации PLC
* @see {@link apps/builder/src/entities/project/object/drivers/BACNET/ui/settings.vue} для выбора PLC в UI
* @see {@link apps/builder/src/entities/project/object/drivers/BACNET/model/entity.ts} для методов преобразования настроек
*/
PLC_ID = "PLC_ID",
SLAVEID = "SLAVEID",
REGISTER = "REGISTER",
PORT = "PORT",
TYPE = "TYPE",
INSTANCE = "INSTANCE",
PROPERTYID = "PROPERTYID",
APPLICATION_TAGS = "APPLICATION_TAGS",
PRIORITY = "PRIORITY",
INTERVAL_SEC = "INTERVAL_SEC",
OID_ADDRESS = "OID_ADDRESS",
REPEAT_SEC = "REPEAT_SEC",
ON_FAILURE_COUNT = "ON_FAILURE_COUNT",
INTER_SEND = "INTER_SEND",
KNX_ADDRESS = "KNX_ADDRESS",
DATA_TYPE = "DATA_TYPE",
IDENTIFIER = "IDENTIFIER",
NAMESPACE = "NAMESPACE",
HTTP_METHOD = "HTTP_METHOD",
HTTP_BODY = "HTTP_BODY",
HTTP_HEADERS = "HTTP_HEADERS",
HTTP_TYPE = "HTTP_TYPE",
HTTP_VALUE = "HTTP_VALUE",
REBOOT = "REBOOT",
CERT = "CERT",
RETRY_COUNT = "RETRY_COUNT",
QOS = "QOS",
NO_LOCAL_FLAG = "NO_LOCAL_FLAG",
RETAIN_AS_PUBLISHED = "RETAIN_AS_PUBLISHED",
RETAIN_HANDLING = "RETAIN_HANDLING",
// Название PLC BACNET для тегов драйверлинка
PLC_NAME = "PLC_NAME",
// Название TYPE для тегов драйверлинка
TYPE_NAME = "TYPE_NAME",
/**
* Транспорт SNMP-устройства (UDP/TCP) в настройках драйвер-линка.
*/
SNMP_TYPE = "SNMP_TYPE",
/**
* Таймаут ответа SNMP-устройства в миллисекундах в настройках драйвер-линка.
*/
TIMEOUT_MS = "TIMEOUT_MS",
/**
* Read community-строка SNMP-устройства ("1"/"2") в настройках драйвер-линка.
*/
READ_COMMUNITY = "READ_COMMUNITY",
}
// #endregionMentioned In
None yet.