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 sensorwould 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:
modelin order to form an eBUS message, a TypeSpecmodelneeds to carry an@iddecorator, 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 CSV files prepared for i18n (from the "archived/" folder) can be done with the npm tasks like this:
npm run csv2tsp: generates English tsp files in directoryouttspincluding a helper filei18n.ymlfor the combination belownpm run csv2tsp-combine: generates German tsp files in directoryouttsp.deincluding a helper filei18n.ymlas well as i18n filesen.yamlandde.yamlinouttspnpm run maintsp: generates themain.tspfile 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, // <= unnamed entry inlines the referenced definition or emits an "!include" instruction (if includes is set in the emitter options)
_include: Hwcmode_inc, // <= entry with a name starting with "_include" explicitly emits an "!include" instruction
named: Hwcmode_inc, // <= named entry emits a !load instruction (if name does not start with "_include")
}
}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-en: generates CSV files in directoryoutcsv/@ebusd/ebus-typespec/- 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.