First, you must identify a data structure you want to read and write. These are often documented in hardware manuals a bit like this one, for the fictional BN-P-6000404 illuminated button panel. We will use the BN-P-6000404 as an example.
Emboss is still beta software. While we believe that we will not need to make any more breaking changes before 1.0, you may still encounter bugs and there are many missing features.
You can contact [email protected]
with any issues. Emboss is not an
officially supported Google product, but the Emboss authors will try to answer
emails.
The Emboss compiler requires Python 3.8 or later -- the minimum supported
version tracks the support timeline of the Python project. On a Linux-like
system with Python 3 installed in the usual place (/usr/bin/python3
), you
can run the embossc script at the top level on an .emb
file to generate
C++, like so:
embossc --generate cc --output-path path/to/object/dir path/to/input.emb
If your project is using Bazel, the build_defs.bzl
file has an
emboss_cc_library
rule that you can use from your project.
The code generated by Emboss requires a C++11-compliant compiler, and a reasonably up-to-date standard library. Emboss has been tested with GCC and Clang, libc++ and libstd++. In theory, it should work with MSVC, ICC, etc., but it has not been tested, so there are likely to be bugs.
The generated C++ code lives entirely in a .h
file, one per .emb
file. All
of the generated code is in C++ templates or (in a very few cases) inline
functions. The generated code is structured this way in order to implement
"pay-as-you-use" for code size: any functions, methods, or views that are not
used by your code won't end up in your final binary. This is often important
for environments like microcontrollers!
There is an Emboss runtime library (under runtime/cpp
), which is also
header-only. You will need to add the root of the Emboss source tree to your
#include
path.
Note that it is strongly recommended that you compile your release code with
at least some optimizations: -Os
or -O2
. The Emboss generated code leans
fairly heavily on your C++ compiler's inlining and common code elimination to
produce fast, lean compiled code.
If you want to contribute features or bugfixes to the Emboss compiler itself, you will need Bazel to run the Emboss test suite.
Next, you will need to translate your structures.
[$default byte_order: "LittleEndian"]
[(cpp) namespace: "bogonel::bnp6000404"]
The BN-P-6000404 uses little-endian numbers, so we can set the default byte
order to LittleEndian
. There is no particular C++ namespace implied by the
BN-P-6000404 user guide, so we use one that is specific to the BN-P-6000404.
The BN-P-6000404, like many devices with serial interfaces, uses a framed message system, with a fixed header and a variable message body depending on a message ID. For the BN-P-6000404, this framing looks like this:
struct Message:
-- Top-level message structure, specified in section 5.3 of the BN-P-6000404
-- user guide.
0 [+1] UInt sync_1
[requires: this == 0x42]
1 [+1] UInt sync_2
[requires: this == 0x4E]
2 [+1] MessageId message_id
-- Type of message
3 [+1] UInt message_length (ml)
-- Length of message, including header and checksum
# ... body fields to follow ...
We could have chosen to put the header fields into a separate Header
structure
instead of placing them directly in the Message
structure.
The sync_1
and sync_2
fields are required to have specific magic values, so
we add the appropriate [requires: ...]
attributes to them. This tells Emboss
that if those fields do not have those values, then the Message
struct
is
ill-formed: in the client code, the Message
will not be Ok()
if those fields
have the wrong values, and Emboss will not allow wrong values to be written into
those fields using the checked (default) APIs.
Unfortunately, BogoNEL does not provide a nice table of message IDs, but fortunately there are only a few, so we can gather them from the individual messages:
enum MessageId:
-- Message type idenfiers for the BN-P-6000404.
IDENTIFICATION = 0x01
INTERACTION = 0x02
QUERY_IDENTIFICATION = 0x10
QUERY_BUTTONS = 0x11
SET_ILLUMINATION = 0x12
Next, we should translate the individual messages to Emboss.
struct Identification:
-- IDENTIFICATION message, specified in section 5.3.3.
0 [+4] UInt vendor
# 0x4F474F42 is "BOGO" in ASCII, interpreted as a 4-byte little-endian
# value.
[requires: this == 0x4F47_4F42]
0 [+4] UInt:8[4] vendor_ascii
-- "BOGO" for BogoNEL Corp
# The `vendor` field really contains the four ASCII characters "BOGO", so we
# could use a byte array instead of a single UInt. Since it is valid to
# have overlapping fields, we can have both `vendor` and `vendor_ascii` in
# our Emboss specification.
4 [+2] UInt firmware_major
-- Firmware major version
6 [+2] UInt firmware_minor
-- Firmware minor version
The Identification
structure is fairly straightforward. In this case, we
provide an alternate view of the vendor
field via vendor_ascii
: 0x4F474F42
in little-endian works out to the ASCII characters "BOGO".
Note that vendor_ascii
uses UInt:8[4]
for its type, and not UInt[4]
. For
most fields, we can use plain UInt
and Emboss will figure out how big the
UInt
should be, but for an array we must be explicit that we want 8-bit
elements.
struct Interaction:
-- INTERACTION message, specified in section 5.3.4.
0 [+1] UInt number_of_buttons (n)
-- Number of buttons currently depressed by user
4 [+n] ButtonId:8[n] button_id
-- ID of pressed button. A number of entries equal to number_of_buttons
-- will be provided.
Interaction
is also fairly straightforward. The only tricky bit is the
button_id
field: since Interaction
can return a variable number of button
IDs, depending on how many buttons are currently pressed, the button_id
field
must has length n
. It would have been OK to use [+number_of_buttons]
, but
full field names can get cumbersome, particularly when the length involves are
more complex expression. Instead, we set an alias for number_of_buttons
using (n)
, and then use the alias in button_id
's length. The n
alias is
not visible outside of the Interaction
message, and won't be available in the
generated code, so the short name is not likely to cause confusion.
enum ButtonId:
-- Button IDs, specified in table 5-6.
BUTTON_A = 0x00
BUTTON_B = 0x04
BUTTON_C = 0x08
BUTTON_D = 0x0C
BUTTON_E = 0x01
BUTTON_F = 0x05
BUTTON_G = 0x09
BUTTON_H = 0x0D
BUTTON_I = 0x02
BUTTON_J = 0x06
BUTTON_K = 0x0A
BUTTON_L = 0x0E
BUTTON_M = 0x03
BUTTON_N = 0x07
BUTTON_O = 0x0B
BUTTON_P = 0x0F
We had to prefix all of the button names with BUTTON_
because Emboss does not
allow single-character enum names.
The QUERY IDENTIFICATION and QUERY BUTTONS messages don't have any fields other
than checksum
, so we will handle them a bit differently.
struct SetIllumination:
-- SET ILLUMINATION message, specified in section 5.3.7.
0 [+1] bits:
0 [+1] Flag red_channel_enable
-- Enables setting the RED channel.
1 [+1] Flag blue_channel_enable
-- Enables setting the BLUE channel.
2 [+1] Flag green_channel_enable
-- Enables setting the GREEN channel.
1 [+1] UInt blink_duty
-- Sets the proportion of time between time on and time off for blink
-- feature.
--
-- Minimum value = 0 (no illumination)
--
-- Maximum value = 240 (constant illumination)
[requires: 0 <= this <= 240]
2 [+2] UInt blink_period
-- Sets the blink period, in milliseconds.
--
-- Minimum value = 10
--
-- Maximum value = 10000
[requires: 10 <= this <= 10_000]
4 [+4] bits:
0 [+32] UInt:2[16] intensity
-- Intensity values for the unmasked channels. 2 bits of intensity for
-- each button.
SetIllumination
requires us to use bitfields. The first bitfield is in the
CHANNEL MASK field: rather than making a single channel_mask
field, Emboss
lets us specify the red, green, and blue channel masks separately.
As with sync_1
and sync_2
, we have added [requires: ...]
to the
blink_duty
and blink_period
fields: this time, specifying a range of valid
values. [requires: ...]
accepts an arbitrary expression, which can be as
simple or as complex as desired.
It is not clear from BogoNEL's documentation whether "bit 0" means the least
significant or most significant bit of its byte, but a little experimentation
with the device shows that setting the least significant bit causes
SetIllumination
to set its red channel. Emboss always numbers bits in
bitfields from least significant (bit 0) to most significant.
The other bitfield is the intensity
array. The BN-P-6000404 uses an array of
2 bit intensity values, so we specify that array.
Finally, we should add all of the sub-messages into Message
, and also take
care of checksum
. After making those changes, Message
looks like:
struct Message:
-- Top-level message structure, specified in section 5.3 of the BN-P-6000404
-- user guide.
0 [+1] UInt sync_1
[requires: this == 0x42]
1 [+1] UInt sync_2
[requires: this == 0x4E]
2 [+1] MessageId message_id
-- Type of message
3 [+1] UInt message_length (ml)
-- Length of message, including header and checksum
if message_id == MessageId.IDENTIFICATION:
4 [+ml-8] Identification identification
if message_id == MessageId.INTERACTION:
4 [+ml-8] Interaction interaction
if message_id == MessageId.SET_ILLUMINATION:
4 [+ml-8] SetIllumination set_illumination
0 [+ml-4] UInt:8[] checksummed_bytes
ml-4 [+4] UInt checksum
By wrapping the various message types in if message_id == ...
constructs,
those substructures will only be available when the message_id
field is set to
the corresponding message type. This kind of selection is used for any
structure field that is only valid some of the time.
The substructures all have the length ml-8
. The ml
is a short alias for the
message_length
field; these short aliases are available so that the field
types and names don't have to be pushed far to the right. Aliases may only be
used directly in the same structure definition where they are created; they may
not be used elsewhere in an Emboss file, and they are not available in the
generated code. The length is ml-8
in this case because the message_length
includes the header and checksum, which left out of the substructures.
Note that we simply don't have any subfield for QUERY IDENTIFICATION or QUERY BUTTONS: since those messages do not have any fields, there is no need for a zero-byte structure.
We also added the checksummed_bytes
field as a convenience for computing the
checksum.
Once you have an .emb
, you will need to generate code from it.
The simplest way to do so is to run the embossc
tool:
embossc -I src --generate cc --output-path generated bogonel.emb
The -I
option adds a directory to the include path. The input file -- in
this case, bogonel.emb
-- must be found somewhere on the include path.
The --generate
option specifies which back end to use; cc
is the C++ back
end.
The --output-path
option specifies where the generated file should be placed.
Note that the output path will include all of the path components of the input
file: if the input file is x/y/z.emb
, then the path x/y/z.emb.h
will be
appended to the --output-path
. Missing directories will be created.
Emboss generates a single C++ header file from your .emb
by appending .h
to
the file name: to use the BogoNEL definitions, you would #include "path/to/bogonel.emb.h"
in your C++ code.
Currently, Emboss does not generate a corresponding .cc
file: the code that
Emboss generates is all templates, which exist in the .h
. Although the Emboss
maintainers (e.g., bolms@) like the simplicity of generating a single file, this
could change at some point.
Emboss generates views, which your program can use to read and write existing arrays of bytes, and which do not take ownership. For example:
#include "path/to/bogonel.emb.h"
template <typename View>
bool ChecksumIsCorrect(View message_view);
// Handles BogoNEL BN-P-6000404 device messages from a byte stream. Returns
// the number of bytes that were processed. Unprocessed bytes should be
// passed into the next call.
int HandleBogonelPanelMessages(const char *bytes, int byte_count) {
auto message_view = bogonel::bnp6000404::MakeMessageView(bytes, byte_count);
// IsComplete() will return true if the view has enough bytes to fully
// contain the message; i.e., that byte_count is at least
// message_view.message_length().Read() + 4.
if (!message_view->IsComplete()) {
return 0;
}
// If Emboss is happy with the message, we still need to check the checksum:
// Emboss does not (yet) have support for automatically checking checksums and
// CRCs.
if (!message_view->Ok() || !ChecksumIsCorrect(message_view)) {
// If the message is complete, but not correct, we need to log an error.
HandleBrokenMessage(message_view);
return message_view->Size();
}
// At this point, we know the message is complete and (basically) OK, so
// we dispatch it to a message-type-specific handler.
switch (message_view->message_id().Read()) {
case bogonel::bnp6000404::MessageId::IDENTIFICATION:
HandleIdentificationMessage(message_view);
break;
case bogonel::bnp6000404::MessageId::INTERACTION:
HandleInteractionMessage(message_view);
break;
case bogonel::bnp6000404::MessageId::QUERY_IDENTIFICATION:
case bogonel::bnp6000404::MessageId::QUERY_BUTTONS:
case bogonel::bnp6000404::MessageId::SET_ILLUMINATION:
Log("Unexpected host to device message type.");
break;
default:
Log("Unknown message type.");
break;
}
return message_view->Size();
}
template <typename View>
bool ChecksumIsCorrect(View message_view) {
uint32_t checksum = 0;
for (int i = 0; i < message_view.checksum_bytes().ElementCount(); ++i) {
checksum += message_view.checksum_bytes()[i].Read();
}
return checksum == message_view.checksum().Read();
}
The message_view
object in this example is a lightweight object that simply
provides access to the bytes in message
. Emboss views are very cheap to
construct because they only contain a couple of pointers and a length -- they do
not copy or take ownership of the underlying bytes. This also means that you
have to keep the underlying bytes alive as long as you are using a view -- you
can't let them go out of scope or delete them.
Views can also be used for writing, if they are given pointers to mutable memory:
void ConstructSetIlluminationMessage(const vector<bool> &lit_buttons,
vector<char> *result) {
// The SetIllumination message has a constant size, so SizeInBytes() is
// available as a static method.
int length = bogonel::bnp6000404::SetIllumination::SizeInBytes() + 8;
result->clear();
result->resize(length);
auto view = bogonel::bnp6000404::MakeMessageView(result);
view->sync_1().Write(0x42);
view->sync_2().Write(0x4E);
view->message_id().Write(bogonel::bnp6000404::MessageId::SET_ILLUMINATION);
view->message_length().Write(length);
view->set_illumination().red_channel_enable().Write(true);
view->set_illumination().blue_channel_enable().Write(true);
view->set_illumination().green_channel_enable().Write(true);
view->set_illumination().blink_duty().Write(240);
view->set_illumination().blink_period().Write(10000);
for (int i = 0; i < view->set_illumination().intensity().ElementCount();
++i) {
view->set_illumination().intensity()[i].Write(lit_buttons[i] ? 3 : 0);
}
}
You can use the .emb
autoformatter to avoid manual formatting. For now, it is
available at compiler/front_end/format.py
.
TODO(bolms): Package the Emboss tools for easy workstation installation.