Module Reference
Topics and Variables
In Sodacan, all topics, and therefore, all messages must be formally defined before they can be used. Furthermore, all variaable*carried by messages in a topic must be defined as well.
A topic defines a schema (or format) of messages for a specific purpose. You can think of a topic as a channel for information flow of similarly formatted messages. Once defined, a topic usually lasts forever, or until manually deleted.
In a simple configuration topics can be created close to where they are commonly published.
MODULE lamp1Control
PUBLISH livingRoom.lamp1 {off,on} AS lamp1
...
THEN lamp1=on
In the example above, the topic livingRoom
and its one variable lamp1
are created automatically with the declaration of the PUBLISH
variable. The module is then free to publish to that topic immediately as shown in the example. In this case, lamp1.on
causes the on value to be published.
But this approach is somewhat restrictive because one module seems to own the topic even though the topic in fact is defined globally.
We can make this much tidier if we create a separate module that defines all or at least some of the topics and variables needed. Nothing else changes in either the producer or consumer. However, a neutral module can be thought of at the owner of the declaration thus allowing the other modules to come and go.
MODULE livingRoomVariables
PUBLISH livingRoom.lamp1 {off,on}
// No other logic in this module
In the above, "livingRoom" is the topic and "lamp1" is a variable that will be exchanged via this topic. The {off,on}
syntax means that the variable will hold an enumerated value of either "on" or "off".
Syntactic Sugar: Sodacan blurs the lines between string literals and identifiers. For example, no quotes are needed in the list of possible values of an enumerated variable. Likewise, the identifier expression
variable.value
is a shortcut. InON
andWHEN
statements, it means the same asvariable == "value"
(true if variable equals the value). In aTHEN
statement, it means the same asvariable = "value"
(assign "value" to variable).
Next, we define a module that produces a message in this topic. In this case, the message is conveying the state of lamp1 as "on" in a message to the named topic.
MODULE button23
PUBLISH livingRoom.lamp1 {on,off} AS lamp
...
ON <some condition>
THEN lamp=on
And, a consumer module interested in this kind of message:
MODULE lamp1
SUBSCRIBE livingRoom.lamp1 AS lamp
ON lamp
AND lamp==on
THEN <do something>
A PUBLISH
declaration has behavior in addition to defining a topic and variable. When the module changes the value of a PUBLISH variable, that variable will be automatically published at the end of the cycle.
A SUBSCRIBE
variable will automatically subscribe to messages with that topic and variable. When a message arrives, the value of that variable will be stored in the module. Then, any ON
statements matching that variable will be executed.
Declaring both a publish and subscribe for the same variable will cause a compile error.
You don't have to be specific about the contents of the message. Here's an example of an event called bedtime. It has no value component. So the ON statement can be equally brief:
MODULE lamp1
SUBSCRIBE bedtime
...
ON bedtime
THEN <do something>
MODULE lamp1
SUBSCRIBE livingRoom.lamp1 {on,off} AS lamp
ON lamp.on
THEN <do something>
ON lamp.off
THEN <do something else>
ON lamp.on
and ON lamp.off
mean the same thing as
ON lamp
AND lamp==on
Module behavior
A module waits quietly for either the passage of time or a message to arrive. If two or more messages arrive at the same time, one is chosen to go first. At that point, the list of AT
(in the case of the passage of time) or ON
(the arrival of a message) statements is considered, one at a time, in the order which they are declared, until one matches. The THEN
statement(s) of the corresponding ON
or AT
is then executed. At that point, no further checks are made of the AT
s and ON
s. Each message or passage of time that is processed by a module is called a cycle
.
Processing then continues to the WHEN
statements. These WHEN
statements are not tied to any message in particular but they do react to changes made from the THEN
s executed above. WHEN
statements are optional but can greatly improve the maintainability of a module. First, a bad example, without using a WHEN
:
MODULE lamp1
SUBSCRIBE mode {off,auto,on}
SUBSCRIBE event {toggle}
PUBLISH state {on,off}
AT midnight ON Fridays // If it's midnight Friday
AND mode==auto // And auto mode is on
THEN state=on // set the lamp state to on
-> THEN log("Lamp is on") // Say so
AT sunset ON Thursdays // If it's sunset on Thursdays
AND mode==auto // and in auto mode
THEN state=on // set lamp state to on
-> THEN log("Lamp is on") // Say so
We can fix this problem by using a WHEN
in the example below. It will send a message regardless of which condition caused the state variable to change.
MODULE lamp1
SUBSCRIBE mode {off,auto,on}
SUBSCRIBE event {toggle}
PUBLISH state {on,off}
AT midnight ON Fridays // If it's midnight Friday
AND mode==auto // And auto mode is on
THEN state=on // set the lamp state to on
AT sunset ON Thursdays // If it's sunset on Thursdays
AND mode==auto // and in auto mode
THEN state=on // set lamp state to on
-> WHEN state==on // If state is on
THEN log("Lamp is on") // say so.
WHEN
only executes when the stated variable changes during a cycle. In the example above, if the state variable was already on
then the WHEN
statement will have no effect.
The passage of time may not trigger any ON
statements. That's normal. However, for messages, if no matching ON
statement is found, then an error is thrown. Why? When a module
subscribes to a particular topic, it declares its intent to deal with that message. If that doesn't happen, there's a problem: Either the SUBSCRIBE
is wrong or the ON
s are wrong or missing.
Module Instance
The examples so far are typically called lamp1, lamp2, etc. suggesting that a different module definition is needed for each device, even if the behavior of all of the devices so numbered are identical. An alternate is to define a module with instances. That is simple enough. Here is an example:
MODULE lamp[location]
SUBSCRIBE mode[location] {off,auto,on}
...
In this example, we made the following changes:
- Change the name of the module to
lamp
(this just looks cleaner, Sodacan doesn't care what you call a module as long as it is unique), - Add the square brackets on the module line. This indicates that the module supports multiple instances.
- The name of a variable in the square brackets will be used to differentiate between different instances.
- It also means that messages we want to subscribe to must specify the location. For example, another module that represents an individual button will need to publish a message that contains the location in addition to naming the variable to publish.
Here is an over-simplified example of an individual button that is not an instance-based module. But you'll see that when it is ready to publish an event, the message will specify the location. In this case, "hallway".
MODULE button3 // A specific button
PUBLISH mode[location] {off,auto,on}
...
THEN mode["hallway"]=auto
If a module can be useful for many different devices but the behavior of each device is the same, then rather than making copies of the module with a slight name change, the module can be written to have module instances.
When a module has instances, a mechanism is needed to create and keep track of such instances. Here is an example of a module that has instances. In this case, location is what differentiates each instance. And you'll notice that the SUBSCRIBE
statement shown is qualified by the name of the instance. In other words, that subscription will listen for messages by location (with a variable name of event
.
MODULE lamp[location]
SUBSCRIBE event[location] {toggle}
...
ON event==toggle
...
MODULE lamp[location]
SUBSCRIBE state[location] {off,on}
SUBSCRIBE location // Respond to a new instance being created
...
ON event==toggle
...
ON location // Respond to a new instance being created
THEN state=off
THEN print(location)
Now, a module can also keep and publish variables and messages module-wide. Think of the module as having a special instance that represents the whole module (A 'static' variable in some languages). But there's a problem: Sodacan never allows peeking into another module which means no peeking into another instance. To solve this problem, we simply send a hidden message from one instance to another including from the top-level instance to it's instances. This is all handled transaparently.
Why so strict even in this situation? Because all instances of one module, including the top-level module, don't necessarily run in the same agent and maybe not even the same server.
This approach works great when the top-level module makes a change that needs to be propagated to the children. However, children to parent is not so obvious. Basically, this should be avoided unless the purpose is to collect statistics such as a count. From an instance, in the following. When the 'count` variable is modified, it is broadcast, as usual.
MODULE lamp[location]
SUBSCRIBE state[location] {off,on}
SUBSCRIBE new.location // Respond to a new instance being created
PUBLISH count 0 // published by the top-level module
SUBSCRIBE count[location] // received by each instance
ON new.location
THEN count++
...
Module Time
Detecting the passage of time is often important for automation problems. Within a module, the AT
statement demonstrates the need for time based events, however, the infrastructure has the responsibility to interpret the requirements specified in a module and respond accordingly. And do it efficiently. One particularly complex aspect is being able to reproduce the passage of time in the future. In other words, we need to be able to look back in time and see that an AT
event was actually triggered.
Conceptually, it looks like this (but don't try this at home). The lines with * are imaginary.
MODULE lamp
*PUBLISH AtNoonOnFridays
...
AT noon ON Fridays // Raise an event at noon on Fridays
*THEN activate(AtNoonOnFridays)
THEN ... what happens at noon on Fridays
*ON AtNoonOnFridays
*THEN ... what happens at noon on Fridays
Here's how this works in Sodacan: Unlike ON
statements, which respond to explicit messages, AT
statements are simply watching a clock looking for a match. To make these time-based events auditable and reproducible, the AT
statements do watch the clock, but when one matches, it doesn't take action directly. Rather, a special message is sent to the module (itself) which then reacts as if it were an ON
statement (This special message is invisible to the module author).
- "AT noon ON Fridays" matches the current time (it is noon and it is a Friday).
- The agent running the module constructs and sends a special message with the id of the matching
AT
statement. - The special message is then processed as usual a moment later. Should the module currently be busy with a different message, then the special message will be processes after that one is done. At this point,
ON
message flow andAT
message flow have been synchronized. - When the special message is processed but the module, the
THEN
statement of the correspondingAT
is executed. If there is aWHEN
statement as part of theAT
statement, it is also evaluated and may result in the special message being ignored. - At this point, the module can go back to listening for new messages.
Then, when needed, there is a complete audit trail in the message stream including hard ON
events and timed AT
events in the order in which they were processed.
Race Condition (not a bug): The special message originates from an instance of the module which may be different from the instance of the module when the special message is processed, due to an intervening update to the module. The time gap is very small and their are most likely no ill effects with one exception: If updating a module results in a deletion of the AT statement, then the special message has nothing to match and will simply be dropped. It would be nice to insure that the special message could synchronously get in front of the queue, but that's not how message serialization works.
Module Instance Time
A slight complication in modules involves AT
statements. If a module is declared as having an instance key and one ore more AT
statements, such as:
MODULE lamp[location]
...
AT noon ON fridays
THEN ...
that means that there is actually one module per instance. And that means that if a time event is triggered, it will be sent to each "instance" of the module. And that may be exactly what is desired. For example, at the time of the time event, the special message is sent to all known instances at that time. Looking back in time, it will be obvious that a newly added instance would not have received the special message.
Module Timers
A timer is used when an action is to be taken in the future. And in many cases, one should be able to cancel or reset the timer. Here's an example:
MODULE lamp1
...
SUBSCRIBE state {off, on}
SUBSCRIBE livingRoom.motion AS motion
TIMER offTimer 30 minutes
...
ON offTimer // If we get a message from the offTimer
THEN state=off // Turn the light off
ON state==on // When state becomes on
THEN offTimer=start // Set a timer to turn it off
ON motion // On motion, reset the timer
THEN offTimer=reset
WHEN state==off // When lamp1 goes off, time is no longer needed
THEN offTimer=cancel
This example is simplified but it does explain the basic timer mechanism.
What is actually happening? The ON state.on
is saying: When the state variable transitions to on
, do the THEN
statement that follows it.
The THEN
statement says to publish the offTimer
message, but not immediately. Rather, Sodacan should wait for 30 minutes before doing so. The Sodacan agent for this module sets up a timer that will send a message to the module (itself) which will behave like any other message. It is perfectly OK to send a message you yourself. The the offTimer message is received, we have an ON state.on
that picks it up and its THEN
says to set the state
to off
.
You'll notice a subtlety in the module compiler: the keyword
ON
begins a statement but it is no longer a keyword within the statement so you are free to use a state name such ason
without conflict.
Because this timer publishes a real message, some other module could also subscribe to the message and take some unrelated action to the offTimer message goes off (separate from the state.off and .on messages). This message also joins the other messages in the topic which forms the historical audit log and maintains the sequential nature of message processing (no side effects).
If for any reason the state is already "off" when the
OffTimer
message is processed, then thestate.off
action has no effect. If the state does transition to off, then anyWHEN
s in the module that react to that state change will trigger as usual.
Module Processing Cycle
ON
and WHEN
statements may seem to work the same way. While it is true that the contents of the statement can look the same, the behavior is very different. During a processing cycle, the first 'AT' to match a change in the module state "wins" and all other AT
s are ignored for that cycle. Conversely, all of the WHEN
s that match during that same cycle are executed.
In a procedural language, the
ON
s are like a series ofIF THEN ELSE
statements and theWHEN
s are like a series ofIF THEN
statements following theIF THEN ELSE
statements.
Module Instance Topic Persistence
How does the Sodacan agent responsible for that module determine all of the module instances to broadcast to when this happens? A separate, parallel to the module, topic is created for each module which contains instances. This topic can be replayed to get the list of topics. There is only one entry per instance of the module that have been created. The following contains the format of messages in this topic all relevant data, the instance name, is in the key:
Key Component | Description |
---|---|
instance | The instance key (for example, location of a light switch) |
This approach is very efficient and does not cause any concurrency issues.
Message-Variable Duality
In Sodacan, a variable defined in a module can be the source or destination for messages. When a message arrives, it is immediately stored in the named variable thus making it available to the module. In the following example, lamp1 is interested in the state of switch 1.
MODULE lamp1
SUBSCRIBE switch1.state {on,off}
...
ON switch1.state==on
THEN ...
ON
statements in the module will react to.
The publishing side is similar. A PUBLISH
variable is a message-in-waiting. Once a processing cycle is completed, PUBLISH
variables that have been modified will be published.
MODULE switch1
PUBLISH state {on,off}
...
ON ...
THEN state==on // Set the state to on
In the background, the Sodacan agent monitors each PUBLISH
variable and, if any changes are made to it by any of the module's THEN
statements, a message will be published containing that variable. In this example, state
is the variable so the message will be published as switch1.state
with a value of on
.
Semantics: You cannot
SUBSCRIBE
andPUBLISH
the same variable.
The message causing the current value of a variable remains with the variable. This makes it easy to access meta-information such as when the message was sent:
MODULE lamp1
SUBSCRIBE switch1.state {on,off}
...
ON switch1.state==on
*THEN system.log#info = switch1.state#timestamp
switch1.state
to the system log".
Module Instantiation
In simple configurations, there may only be a single instance of each type of module. One living room lamp, one living room light switch, etc. In this case, messages will have an empty key
attribute. Other modules can be configured to handle a class of devices. For example, an organization might have a single lighting configuration which is used in all (or most) locations. Each office, for example, could behave the same but independent of other offices. In this case, the `'key' attribute of a message will contain the office (or location) name. Not much changes when a module is representing a class of devices rather than a single device. The module name would normally change. Instead of
MODULE JoesOfficeLight
a more appropriate module name in this case will be
MODULE OficeLight[location]
MODULE OficeLight[location]
PUBLISH state[location] {on,off}
state
variable is separate for each location.
While the state variable (and consequently messages) are per-location, we might need other variables that apply to the entire class. Consider a company that has a policy of putting all lights into auto mode at a certain time of day requiring motion detecting for the light to remain on. That time is set company-wide. In this case we would like to send a single message to the "OfficeLight" module with the time all offices should go into auto mode.
MODULE OficeLight[location]
SUBSCRIBE autoModeOnTime 00:00
PUBLISH state[location] {on,off}
autoModeOnTime
variable has no key associated with it. A subsequent AT statement
will refer to autoModeOnTime
, without a key qualifier.
MODULE OficeLight[location]
SUBSCRIBE autoModeOnTime 00:00
PUBLISH mode[location] {auto,manual}
PUBLISH state[location] {on,off}
AT autoModeOnTime
THEN mode[location]=auto
...
Adapters
A Sodacan adapter is an end node in a Sodacan implementation. There are two primary types of adapter: message consumer and message producer. However, adapters can also be both a consumer and producer at the same time. The following is a very simple interaction between a lamp and a button and a module that controls the behavior of the lamp (on or off). A real-world example would likely have additional capabilities but we keep it simple here:
sequenceDiagram
buttonAdapter->>messageBus: button.press
messageBus->>lampModule: button.press
lampModule->>messageBus: lamp=on
messageBus->>lampAdapter: lamp=on
Flow of control:
1. buttonAdapter
running on a microcontroller such as a Raspberry PI, monitors a digital in pin and when it goes positive (ignoring debounce logic), a message is published to the Sodacan messageBus
.
2. The message is delivered to the lampModule
which has subscribed to this type of message.
3. The lampModule
determines if the button press is and off or on transition (it keeps track of the state of the lamp).
When the state of the lamp in the lampModule
changes, another message containing the new state is published to the messageBus
.
4. The lampAdapter
, running on a microcontroller subscribes to lamp's state message and upon receipt of this message sets a digital output pin high or low depending on the content of the message.
If there is no need for logic in the lampModule, then it can be eliminated and the message published by the button read directly by the adapter module, like this:
sequenceDiagram
buttonAdapter->>messageBus: lamp=on
messageBus->>lampAdapter: lamp=on
Technically, an adapter is really just another module that has a bit of low-level code attached to it. It is also may be tied to a specific host if necessary. The low-level code (C, Java, etc) handles the details of device access: DIGITAL IN, OUT, SPI, D-A, A-D, etc. It also can do database IO or whatever else one can imagine. In the example below, the adapter module waits for a message about the state of the lamp
. As usual, the ON
detects the message, updates the variable with the new value and THEN
makes a function call:
MODULE lamp3
SUBSCRIBE livingRoom.lamp AS lamp
EXTERNAL someFunction
ON lamp
THEN someFunction
someFunction
function call returns, the cycle is complete. The module then waits for the next message.
Message Persistence
When a message is produced, it takes on a life of its own; Neither belonging to the producer nor to any of its potential consumers. At that point, the message is owned and stored (persisted) by the message bus. There is no sure-fire way for Sodacan to know when a message has been completely consumed. For example, a module that might consume a particular type of message may not exist yet. If resources were infinite, there is no reason Sodacan would need to recover space used by any messages. The messages within a topic can come and go. Indeed, most topics define the lifetime of messages contained within that topic.
Consider, for example, that we want to add a new module to an existing configuration that reports on the average number of uses of a certain button per month. In a traditional system, the data could be a challenge to create. Historical data may not even exist. But in Sodacan, the data already exists in the topic that was used to get the button press notification to the lamp that is controlled by that button press. (Just because the message was consumed by one module does not mean that the message will be discarded). So, the new reporting module simply subscribes to that same topic and it will get all of the messages from the past in chronological order.
Now, Sodacan has several ways to deal with old messages in a topic. One can set an expiration date for a particular topic: Messages older than a certain number of days, weeks, months, or years will be automatically deleted. Or, when a topic exceeds a certain size, older messages can be deleted. Finally, one can just let the messages accumulate forever. Consider that many messages in a Sodacan application are quite small. Our button activation message will be about 50 bytes long. If we press that button 50 times per day, every day for a year, that would add up to less than one megabyte of data. Therefore, it's probably not worth cleaning up this type of message if there is even a small change of using that data in the future. On the other hand, messages from a security camera are much larger and so the topic should probably be purged either based on size (a very safe option) or the age of messages.
System Messages
The messaging system is also used for administrative and operational purposes. Any agent running a module or an adapter routes error messages to a log topic. Sodacan uses an administrative topic to deploy modules to the appropriate agent/server. Therefore, in a clustered setup, it is not necessary to manually keep application files on individual servers. By default, Module persistence is also kept in an administrative topic.
Topic | Description |
---|---|
xxx | All mode commands are broadcast on this topic, see CLI for details |
module |
Special Variables
In addition to variables that you explicitly define in your module, some are special in that they provide access to the agent, system, or custom plugins. Variables can also have attributes. Most special variables provide access to plugins. Some attributes are actually attributes on all variables. For example, a SUBSCRIBE variable provides access to when the message was created, which module (or plugin) sent it, etc.
Special attribute names are preceded by a hash (#) symbol. for example, assuming the following is a SUBSCRIBE variable named "mode", `mode.#msgid' will access the message id of the last message that set that variable. Those marked read are read-only. read/write attributes can be assigned to. Some are write only write.
Note: A module variable is a variable defined in a module: publish, subscribe, private, or topic.
variable | Attribute | read/write | Description |
---|---|---|---|
any subscribe variable | #timestamp | read | The timestamp (UTC Z-time) of the message |
any subscribe variable | #msgid | read | The timestamp (UTC Z-time) of the message |
any subscribe variable | #topic | read | The topic of the message |
any subscribe variable | #namespace | read | The topic of the message |
any subscribe variable | #version | read | The message format version |
any module variable | #type | read | The type of variable (string, number, boolean, etc) |
any module variable | #modified | read | True if modified in this cycle |
any module variable | #initialValue | read | The initial value of the variable as declared |
any module variable | #constraints | read | The constraints specified for this variable. |
any publish variable | #modified | read | True if modified in this cycle and will be published at the conclusion of the cycle |
system.config | #latitude | read | Latitude of this location |
system.config | #longitude | read | Longitude of this location |
system.config | #timezone | read | Timezone of this location |
system.config | #locationName | read | Name of this location |
system.config | #locationAddress | read | Address of this location |
system.clock | #month | read | The month of the current datetime |
system.clock | #day | read | The day of the current datetime |
system.clock | #year | read | The year of the current datetime |
system.clock | #hour | read | The hour of the current datetime |
system.clock | #minute | read | The minute of the current datetime |
system.clock | #second | read | The seconds of the current datetime |
system.clock | #sunrise | read | Today's sunrise |
system.clock | #sunset | read | Today's sunrise |
system.clock | #season | read | The current season |
system.clock | #season | read | Today's sunset |
system.log | # info | write | Write informational text to system log |
system.log | # warn | write | Write warning text to system log |
system.log | # alert | write | Send general alert message and log it |
system.module | #name | read | Name of this module |
system.module | #eventType | read | message or clock event |
system.module | #source | read | Current source code of module |
system.agent | #name | read | Name of the agent hosting this module |
rpi4.gpio2 | # mode | read/write | Set mode for GPIO 2 |
rpi4.gpio2 | # pin | read/write | Set/get pin for GPIO 2 |
dmx.fixture2 | #red | write | Set the red color value in (lighting) fixture2 |