csp_proc

Lightweight, programmable procedures with a libcsp- and libparam-native runtime.

View the Project on GitHub discosat/csp_proc

Geo-fencing

In this example, we will imagine a scenario where a vehicle is equipped with a GNSS module exposed via libparam parameters on node 1, and node 2 has some active component that needs to be controlled based on the vehicle’s position, e.g. a sensor logging a data point when the vehicle enters a certain area. The following is a sequence of commands that can be used to implement a simple geo-fencing procedure based on manhattan distance from a fixed point. For this example, it’s assumed that node 1 has the libparam parameters: lat (double), lon (double) from the GNSS module along with lat_diff (double), lon_diff (double), dist (double) and geo_check (uint8) to store intermediate results, and target_lon (double) target_lat (double), max_dist (double) to determine the geo-fencing area. Node 2 has a sensor_log (uint8) parameter with a callback attached to trigger the logging of a data point.

# procedure 0 (geo-fencing setup)
proc new

proc binop lat - target_lat lat_diff 1  # calculate latitude difference
proc ifelse lat_diff < 0 1  # negate if negative
proc unop lat_diff - lat_diff 1
proc noop

proc binop lon - target_lon lon_diff 1 # calculate longitude difference
proc ifelse lon_diff < 0 1  # negate if negative
proc unop lon_diff - lon_diff 1
proc noop

proc binop lat_diff + lon_diff dist 1
proc ifelse dist < max_dist 1  # check if the vehicle is within range
proc set geo_check 1 1  # geo_check signals the vehicle is within the area
proc set geo_check 0 1  # geo_check signals the vehicle is outside the area
proc call 0 1 # call itself recursively to keep checking the vehicle's position

proc push 0 1  # push the procedure to slot 0 on node 1

# procedure 1 (geo-fencing check)
proc new
proc block geo_check == 1 1  # block until vehicle is within the area
proc set sensor_log 1 2  # log a data point
# optionally call itself recursively if the sensor should keep logging data points while the vehicle is in the area
proc call 1 2

proc push 1 1  # push the procedure to slot 1 on node 1

Note that the last integer argument in the proc commands is the node on which the operands are located. The procedures themselves should be pushed onto the same node hosting a procedure server, which is assumed to be node 1 in this case.

The following commands can now be executed to set up and run the location-based logging procedure:

node 1
set target_lat 55.6761
set target_lon 12.5683
set max_dist 0.01
proc run 0  # Run the procedure to update the geo_check parameter
proc run 1  # Run the procedure to log data points when the vehicle is within the area

One can then imagine an extended scenario where there is e.g. a third node responsible for some actuator that must react based on the sensor node, which adds another layer of coordination to the system - all this can be orchestrated using the DSL!

Utilizing reserved, pre-programmable procedure slots

There is also support for pre-programmed procedures in reserved slots, which can be used to simplify the DSL code or take care of more complex operations. For example, one can write a function compiled into the application on node 1 that calculates the euclidean distance between points defined by (lat, lon) and (target_lat, target_lon) and stores the result in the dist parameter. For the sake of example, let’s assumed this procedure is available in slot 0. Then the geo-fencing setup procedure can be simplified as follows:

# procedure 1 (geo-fencing setup)
proc new
proc call 0 1  # call the pre-programmed procedure to calculate `dist`
proc ifelse dist < max_dist 1  # check if the vehicle is within range
proc set geo_check 1 1  # geo_check signals the vehicle is within the area
proc set geo_check 0 1  # geo_check signals the vehicle is outside the area
proc call 1 1  # call itself recursively to keep checking the vehicle's position

proc push 1 1  # push the procedure to slot 1 on node 1

The (geo-fencing check) procedure can then simply be adjusted to account for the new procedure slot. To demonstrate more functionality, we can do this by running the following slash commands before redefining and pushing the procedure above.

node 1  # set the active node to node 1 for implicit node argument

proc pull 1  # pull the pre-programmed procedure from slot 1
proc pop  # remove the last instruction in the procedure
proc call 2  # replace the call instruction to point to new slot

proc push 2  # push the procedure to slot 2

Fibonacci Sequence

While using the DSL to calculate Fibonacci numbers certainly isn’t the intended use of the DSL (nor efficient), it serves as a good example to demonstrate the capabilities of the DSL. The following is a sequence of commands that can be used to calculate the Fibonacci sequence up to the $n$-th term:

# procedure 0 (initialization)
proc new
proc set _zero 0  # initialize the _zero parameter
proc set rx0 0  # initialize register 0 to hold the n-th term
proc set rx1 1  # initialize register 1 to hold the (n+1)-th term
proc ifelse n > _zero  # only work to do if n > 0
proc call 1  # call procedure 1, defined below

proc push 0  # push the procedure to slot 0

# procedure 1 (calculation)
proc new
proc binop rx0 + rx1 rx2  # calculate the (n+2)-th term
proc unop rx1 idt rx0  # shift the registers one term with identity operator
proc unop rx2 idt rx1
proc unop n -- n  # decrement n
proc ifelse n == _zero
proc noop  # if-clause: condition is met
proc call 1  # else-clause: condition is not met (recurse in this case)

proc push 1  # push the procedure to slot 1

E.g. to calculate the 10th term of the Fibonacci sequence, the following commands can now be executed:

set n 10
proc run 0

After which the 10th term of the Fibonacci sequence can be read from the rx0 register (which is really just the rx0 libparam parameter).

get rx0

Naturally, this assumes n, _zero, rx0, rx1, and rx2 are available as integer libparam parameters on the node. Also note that with the default FreeRTOS-based runtime, it is recommended to divide complex routines into small units of work, where any calls to other procedures are done in the last instruction (or second-last if preceded by ifelse) to avoid nesting function calls.