TypeSpec is a language for defining data models and can be extended for domain specific use.
As an eBUS message is basically nothing more than a transfer of a (rather small) data model between two participants, TypeSpec together with the eBUS TypeSpec library is a good way of describing and documenting eBUS message definitions.
Since there is already a lot of tools around TypeSpec including language support in VS Code, autocomplete, formatting, linting, and even conversion to JSON schema or others, it is a lot more convenient to work with message defintions this way.
For details of the eBUS TypeSpec library, see the library readme.
In addition to the tooling provided by TypeSpec directly, the VS Code extension "ebus notebook" is provided and allows working on message definitions directly in a VS Code notebook with support for sending to a running ebusd instance.
Here is a small example illustrating the enhanced readability. A message declared in CSV like this:
r,ehp,SourceTemp,source temperature,,08,"B509","0D0F00",,,tempsensor,,,Source temperature sensor
would be written as TypeSpec file like this:
import "@ebusd/ebus-typespec";
import "./_templates.tsp";
/** source temperature */
@zz(0x08)
@id(0xb5, 0x09, 0x0d, 0x0f, 0x00)
model SourceTemp {
/** Source temperature sensor */
value: tempsensor;
}
Furthermore, when adding another level of abstraction, this can be even reduced to this (the template model Register<typ>
not shown here as this would be part of the _templates.tsp
, see the manufacturer specific subdirectories in src/
for a complete example):
import "@ebusd/ebus-typespec";
import "./_templates.tsp";
/** source temperature */
@ext(0x0f, 0x00)
model SourceTemp is Register<tempsensor>;
The primary source for definitions is the src
folder, the content of which was initially generated from the latest CSV files using the conversion tool.
It consists only of .tsp
files with the declarations as well as optionally one i18n YAML file per supported language, e.g. en.yaml
.
The declarations are supposed to be written in English only and for translation the comments from the declarations can be mapped to a language using the i18n YAML.
The top level directory may only contain message and template declarations common for any manufacturer where sub directories are supposed to contain only declarations specific for that manufacturer.
This is a list of associations from eBUS structures to the corresponding TypeSpec declaration:
- circuit:
namespace
- message:
model
in order to form an eBUS message, a TypeSpecmodel
needs to carry an@id
decorator, either directly or inherited via@base
/@ext
- field:
model property
- message/field comment: normal code comment before the entity, i.e.
/** comment goes here */ entity...
Each .tsp
file basically looks similar, i.e. starts with imports and uses, followed by the manufacturer and circuit namespaces and carrying therein the circuit specific messages.
In order to ease the move to TypeSpec,the csv2tsp.ts
utility utility is available for converting existing CSV files.
Converting the latest CSV files prepared for i18n can be done with the npm tasks like this:
npm run csv2tsp
: generates English tsp files in directoryouttsp
including a helper filei18n.yml
for the combination belownpm run csv2tsp-combine
: generates German tsp files in directoryouttsp.de
including a helper filei18n.yml
as well as i18n filesen.yaml
andde.yaml
inouttsp
npm run maintsp
: generates themain.tsp
file from the list of the other tsp filesnpm run format
: reformats all generated tsp filesnpm run lint
: checks for issues in all generated tsp files
Alternatively, the npm run csvall
performs all of these steps in addition to some pre-normalization, and also adjusts/excludes some top level items that rarely changed and emit warnings otherwise.
From here, the changed declarations in outtsp
can be compared against the current source folder in src
, changes to it applied and reviewed beforw PR submission.
- embrace comments where reasonable
- avoid repetition in comments, e.g. do not use a comment that only contains exactly the message or field name
- do not put models to deep in the type graph
- ensure the tsp compiler does not emit diagnostics
- use the formatter (see
npm run format
) - combine read/write active/passive messages where possible
- mark incoming only fields as optional
- name a single field in a message
value
- use a common top level namespace per manufacturer
- use a unique namespace per circuit (under the manufacturer namespace)
- use a unique name per message
- reuse shared messages with the include schema
- prefer predefined types instead of redefining them
- only eBUS internal scalar type definitions are allowed to be uppercase only (like
UCH
)
Inclusion of shared common models is a bit tricky, as TypeSpec currently lacks good support for doing so out of the box. Therefore, a special union is currently used that implicitly resolves to (i.e. includes) all the models of namespaces listed in it, e.g.
import "@ebusd/ebus-typespec";
import "./hwcmode_inc.tsp"; // <= imports the `Hwcmode_inc` namespace for referencing it below
namespace Circuit {
/** included parts */
union _includes {
Hwcmode_inc, // <= references the imported namespace and implicitly resolves to all contained models
named: Hwcmode_inc, // <= named entry emits a !load instruction instead
}
}
See the style guide in the eBUS TypeSpec library.
The final step is to create the CSV needed by ebusd (for the time being):
npm run compile
: generates CSV files in directoryoutcsv
- this directory would then serve as the base for publishing to CDN for use by ebusd
The good thing about TypeSpec is, that declarations can easily be emitted as e.g. JSON Schema or even OpenAPI. See the TypeSpec docs for details.