-
Notifications
You must be signed in to change notification settings - Fork 15
Hexabus Compiler
State machines are executed by a special interpreter running on Hexabus devices. State machines can write the local endpoints of the device and react to packets (and thus, sensor values) broadcast by other devices in the network. Each state machine has a number of states, each of which contains a set of event blocks that specify how the machine reacts to events. The code is specified in minimal C-like language. If an event occurs the state machine interpreter will execute the corresponding event block, change to a different state if necessary and suspend execution until the next event occurs. In order to ensure timely termination of each execution, the language does not support loops.
All identifiers used in the state machine are C-style. They must contain only underscores, alphabet letters and digits, and they must not start with a digit. State names have their own namespace, all other identifiers share a common namespace.
Comments have the same form as in C/C++.
// Single line comment
/*
Multi
Line
Comment
*/
Each variable and each endpoint used in a state machine has have one of the following data types:
- uint8
- uint16
- uint32
- uint64
- int8
- int16
- int32
- int64
- bool
- float
Values of one type can be explicitly cast to values of any other type. Casts are
not necessary when widening a value (casting to a type that can represent all
values of the source type), while narrowing casts must always be written
explicitly.
Overflow in signed types yields undefined behaviour. Unsigned types perform all
calculations in modulo arithmetic, thus unsigned overflow is always defined.
Casting a finite float value to integer types first truncates the original
value, then applies overflow rules, casting non-finite floats (infinities or
NaN) to integer types is undefined. Casting float values to bool is defined as
x == x && x != 0
.
Casting any integer type to bool is equivalent to x != 0
.
Additionally, types can be specified via typeof expressions. These expressions
take the form typeof(<expression>)
and yield the type of the expression.
typeof is an unevaluated context and behaves much like decltype in C++.
Example:
uint32 x = 0;
x = x - 1; // defined, x now has value 2^32 - 1
int64 y = x; // defined, y has value 2^32 - 1
int32 z = int32(x); // undefined, 2^32 - 1 does not fit int32
int32 w = 2147483647; // defined, largest value allowed for int32
w = w * 2; // undefined, signed overflow
float f = 3.1415206;
uint8 pi = uint8(f); // pi = 3;
Each endpoint has to be declared before it is used in a device definition or a
state machine code block. For every endpoint ID <eid>
, only one endpoint
definition may exist.
endpoint <name>(<eid>) : <type> (<access levels>);
- name: identifier for the endpoint
- eid: ID of the endpoints
- type: data type of the endpoint
- access levels: comma separated list of access levels
Valid access levels are:
- write: the endpoint can be written by the state machine
- read, global_write: currently unused
- broadcast: the device will periodically broadcast the value of this endpoint.
Whenever a device broadcasts the value of one of its endpoints, other devices will run their state machine for the new value. Only values of endpoints marked as broadcast can be used by the state machine, and the values of such endpoints can only be retrieved with on update blocks.
endpoint Switch(1) : bool (write);
endpoint Button(4) : bool (broadcast);
endpoint ThingWithAState(4242) : bool (read,write);
Each device has to be declared before it can be used inside state machine code blocks.
device <name>(<ipv6>) : <endpoint list>;
- name: identifier for the device. It can be used later in the code to refer to this device an its endpoints.
- ipv6: the ipv6 address of the device
- endpoint list: a comma separated list of endpoints for the device.
device dev1(fd83:42::50:c4ff:fe04:810c) : Switch, Button;
device dev2(fd83:42::50:c4ff:fe04:1e4) : Button;
The machine declaration contains the actual code for the state machine. In general a machine declaration looks like this:
machine <name> {
<global variables>
state <name> {
... event blocks ..
}
state <name> {
... event blocks ..
}
...
};
The machine keyword introduces a new machine, where name is an identifier. A machine declaration also contains a machine body, which describes the global variables and states of the machine.
Global variables are available to all states of the machine. A single global
variable is declared as <type> <name> = <value>;
, where type is a data
type, name is an identifier and value is a valid value for the type.
Each machine declaration must contain at least one state. The first state of a machine declaration will be used as the initial state during startup. When a device resets its state machine, it will be in the initial state when the first event occurs (though the initial state is not entered during reset).
A state block consists of multiple event blocks. Each event block contains code to be run only when the machine is in that state, subject to further restrictions (see Event blocks). The event blocks of a state are run whenever matching events occur while the state machine is in that state.
As with machines, it is possible to declare state-local variables that will be available to all code in the state and retain their values until the state is left. State-local variables are declared like global variables, but initialization is performed only when the state is entered.
state <name> {
<state-local variables>
on entry {
... code ...
}
on update from <device>.<endpoint> {
... code ...
}
...
on periodic {
... code ...
}
on exit {
... code ...
}
always {
... code ...
}
}
There are five types of event blocks, four of which are conditional. The conditional types of event blocks are on entry, on update, on periodic and on exit, the unconditional event block is *always
Their order within a state is not important, but the always block, if it exists, must be the last event block of the state, and only one always may exist per state.
When two conditional event blocks for the same event exist, the compiler will automatically merge them. All on entry blocks will be concatenated to produce a single on entry block, the same happens for on exit and on periodic blocks. Merging of on update is described later.
The code inside an on entry block is called whenever the state is entered by a goto command.
It is important to note that the default state of a state machine is not entered via a goto instruction if a state machine reset occurs, thus the on entry block of the initial state will not be executed in case of a reset. Initialization of global variables and state-local variables of the initial state will occur during reset.
on update from <device>.<endpoint> {
... code ...
}
The code inside an on update is executed whenever the device running the state machine receives a broadcast from the specified device and endpoint.
Two on update blocks can be merged if and only if the refer to the same device and the same endpoint. If two on update blocks can be merged, the compiler will merge them.
on periodic {
... code ...
}
The code inside an on periodic block will be executed periodically, about once per second, as long as the state machine is in the declaring state.
always {
... code ...
}
The code inside an always block will be executed whenever the state machine receives a packet from another device, whenever the state machine does a periodic check (see on periodic), or during reset when the always block is contained in the initial state of the machine.
on exit {
... code ...
}
The on exit block is executed whenever the state is left by a goto command.
Expressions in state machines are almost exactly as in C, with a few exceptions:
- there are no pointer or array types
- narrowing (e.g. int32 -> int8) is always explicit (via cast notation)
- typecasts use the C++ constructor style, i.e.
type(<value>)
instead of(type) <value>
Multiple statements can be grouped into one statement block, which is again a statement.
{
<statement0>
<statement1>
...
}
Variables can be declared like global or state-local variables at any point where a statement is allowed. Whenenver execution reaches a variable declaration, the variable is initialized; from this point on, the variable can be used. The variable ceases to exist when its enclosing block is left.
The if statement expects a boolean expression as condition and is followed by a code block to be executed when the condition is true, and optionally a code block to be executed when the condition is false.
if (<condition0>) {
... code ...
}
if (<condition1>) {
... code ...
} else {
... code ...
}
Switch statements work as in C, but each label sequence marks only a single
statement. There is no fall through to other statements or labels, and multiple
statements for one label sequence are not allowed.
Case labels can be any kind of integral constant expressions, as long as their
type matches the type of the expression in the switch statement. Constant
expressions are the boolean literals true
and false
, all integer literals,
and all expressions, except function calls, that contain only constant
expressions.
switch(expression) {
case constexpr1:
statement0;
case constexpr2:
{
statement1;
statement2;
}
case constexpr3:
case constexpr4:
statement3;
default:
statement4;
}
Goto terminates the execution of the current event block and switches to a different state. After terminating the current event block, the exit block of current state is executed. The interpreter will then switch to the new state and execute the entry block for the new state before suspending execution until the next event.
Goto statements are not allowed inside entry and exit blocks.
goto <stateindetifier>;
- int64 now(): Returns a unix timestap as int64.
- int32 second(int64 timestamp): Second part of the timestamp (0 - 59)
- int32 minute(int64 timestamp): Minute part of the timestamp (0 - 59)
- int32 hour(int64 timestamp): Hour part of the timestamp (0 - 23)
- int32 day(int64 timestamp): Day of the month (1 - 31)
- int32 month(int64 timestamp): Month (0 - 11)
- int32 year(int64 timestamp): Years since 1900
- int32 weekday(int64 timestamp): Days since Sunday (0 - 6)
Class declarations are basically machine declarations with one or more class parameters, which have to be set upon instantiation. Inside a class declaration, class parameters and literals are indistinguishable.
machine class <classname>(<templatetype> <identifier>, ...) {
... same as machine definition ...
};
// Instantiation
machine <machinename> : <classname>(<argument>, ...);
Classname and machinename are c-style identifiers.
templatetype can be of:
- data type (e.g. bool, uint8, ...). During instantiation, the argument for such a parameter must be a constant expression.
- endpoint. Argument must name an endpoint.
- device. Argument must name a device.
- device[]. Argument must be a non-empty list of devices, enclosed in [].
Device lists may be used instead of plain devices in writes and on update
blocks. When used in writes, the given endpoint will be written on all devices
in the list. When used in on update
blocks, the state machine will wait for
updates from any device in the list.
machine class Toggle(device inputdev,
endpoint inputep,
device[] ouputdev,
endpoint outputep,
bool initial) {
state init {
always {
if (initial) {
goto switched_on;
} else {
goto switched_off;
}
}
}
state switched_on {
on entry {
ouputdev.outputep = true;
}
on update from inputdev.inputep {
//Assuming inputep is a button
goto switched_off;
}
}
state switched_off {
on entry {
ouputdev.outputep = false;
}
on update from inputdev.inputep {
//Assuming inputep is a button
goto switched_on;
}
}
};
machine lamp1: Toggle(lightswitch, button, [lamp], relais, false);
Behaviour of devices can be controlled not only using machines, but also, in a more restricted setting, using behaviours. While machines specify how a device interacts with the world and add functionality to a set of devices, behaviours add functionality to a specific device. Each behaviour is restricted to one device and cannot interact with other devices.
Behaviours add functionality by adding synthetic endpoints to a device that can be read or written, along with some state (if desired) and the ability to react to events on the current device. A behaviour looks a lot like a machine class:
behaviour <identifier> on <device> {
... variables ...
... endpoints ...
... event blocks ...
};
Each behaviour is given for one specific <device>
given in its definition. All
event blocks allowed in states are also allowed in behaviours, with the
exception of on exit
. The on entry
block of a behaviour runs when the state
machine is reset, on update
blocks can only acquire values from <device>
,
which can be accessed using the identifier this
within the behaviour block.
An endpoint in a behaviour, like an endpoint at the toplevel, must specify a
datatype and accessors. If a read
block is not given for an endpoint, the
endpoint cannot be read. The read
block must be terminated with an expression,
which gives the value read from the endpoint. If a write
block is not given,
the endpoint cannot be written. In a write
block, the value written to the
endpoint is available in the predefined identifier value
.
Within an endpoint, only variables declared earlier in the behaviour are available. Other endpoints of the same behaviour cannot be read or written, but endpoints of behaviour previously declared for the same device can be.
endpoint <identifier> : <type> {
read {
... code ...
<expression>
}
write {
... code ...
}
}
Endpoints specified in a behaviour can read and written using the behaviour-qualified name of the endpoint, e.g.
behaviour b on dev {
endpoint X : bool {
read { true }
write { }
}
};
machine m {
state init {
always {
if (dev.b.X)
dev.b.X = false;
}
}
}
Behaviour classes are to behaviours what machine classes are to machines. A behaviour class is declared much like a machine class:
behaviour class BC(...parameters...) {
endpoint X : bool { ... }
};
behaviour B on dev : BC(...argument...);
Unlike machine classes, the parameter list of a behaviour class may be empty. When a behaviour was instantiated from a class, the endpoints of the behaviour may also be referred to using the class name instead of the instance name. Like machine classes, a behaviour class may be instantiated multiple times, and even multiple times for the same device.
// in machines/later behaviours:
dev.BC.X = true;
// in machine instantiations:
machine m : mc(BC.X);
If a behaviour class is instantiated more than once for a given device, class-qualified endpoint look becomes and ambiguous and is thus not allowed.
behaviour class BC() {
endpoint X : bool { ... };
};
behaviour B1 on dev : BC();
behaviour B2 on dev : BC();
// not allowed, BC.X could be either B1.X or B2.X
dev.BC.X = true;
// not ambiguous, allowed
dev.B1.X = true;
dev.B2.X = true;
endpoint Switch(1) : bool (write, broadcast);
endpoint Button(4) : bool (broadcast);
device dev1(fd83:42::50:c4ff:fe04:810c) : Switch, Button;
device dev11(::1) : Switch;
device dev2(fd83:42::50:c4ff:fe04:1e4) : Button;
device dev3(::3) : Button;
behaviour class Toggle(endpoint ep) {
bool s = false;
endpoint Do : bool {
write {
this.ep = s;
s = !s;
}
}
on update from this.ep {
s = this.ep;
}
};
behaviour t on dev1 : Toggle(Switch);
machine class c(typeof(dev1.Switch) v) {
int64 since = now();
state s_off {
on update from dev1.Button {
dev1.Toggle.Do = v;
goto s_on;
}
}
state s_on {
int64 since = now();
on update from dev1.Button {
dev1.Toggle.Do = !v;
goto s_off;
}
on periodic {
if (now() - since > 1) {
dev1.Switch = false;
goto s_off;
}
}
}
};
machine m1 : c(true);