Filter and transform
You can let the OOCSI server do some computing work for you and this works on ALL platforms that OOCSI supports: by subscribing from a client to a channel with additional filter and/or transform expressions, you can control which messages will be received on the client (“filter”) and/or you can compute new values based on the message contents (“transform”).
Filtering messages
Let’s start with a filter example. Suppose an environmental sensor publishes events with temperature and humidity to a channel climate. We only want our client to wake up when temperature > 25:
// Java / Processing
oocsi.subscribe("climate[filter(temperature > 25)]", "handleClimate");
# Python
oocsi.subscribe("climate[filter(temperature > 25)]", handle_climate)
// JavaScript (oocsi-web.js)
OOCSI.subscribe("climate[filter(temperature > 25)]", (msg) => {
console.log("High temperature alert:", msg.data.temperature);
});
// ESP32 (Embedded C++ / Arduino)
oocsi.subscribe("climate[filter(temperature > 25)]", handleClimate);
You can also use boolean logic to create compound conditions:
// Java / Processing: Filter for hot AND humid conditions
oocsi.subscribe("climate[filter(temperature > 25 && humidity > 70)]", "handleClimate");
Transforming messages
With transform, you can compute new values directly on the server before the message reaches your client. The result can be stored in a new key or replace an existing key.
For instance, converting a temperature from Celsius to Fahrenheit:
// Java / Processing: Add a new fahrenheit field: (temp * 1.8) + 32
oocsi.subscribe("climate[transform(fahrenheit, (temperature * 1.8) + 32)]", "handleClimate");
# Python
oocsi.subscribe("climate[transform(fahrenheit, (temperature * 1.8) + 32)]", handle_climate)
// JavaScript (oocsi-web.js)
OOCSI.subscribe("climate[transform(fahrenheit, (temperature * 1.8) + 32)]", (msg) => {
console.log("Fahrenheit:", msg.data.fahrenheit);
});
// ESP32 (Embedded C++ / Arduino)
oocsi.subscribe("climate[transform(fahrenheit, (temperature * 1.8) + 32)]", handleClimate);
Combining filter and transform
You can chain multiple filter and transform expressions together in a single subscription by separating them with semicolons ;:
// Java / Processing: Filter for temperature > 25 AND compute fahrenheit
oocsi.subscribe("climate[filter(temperature > 25); transform(fahrenheit, (temperature * 1.8) + 32)]", "handleClimate");
# Python
oocsi.subscribe("climate[filter(temperature > 25); transform(fahrenheit, (temperature * 1.8) + 32)]", handle_climate)
// JavaScript (oocsi-web.js)
OOCSI.subscribe("climate[filter(temperature > 25); transform(fahrenheit, (temperature * 1.8) + 32)]", (msg) => {
console.log("Filtered & Transformed:", msg.data);
});
// ESP32 (Embedded C++ / Arduino)
oocsi.subscribe("climate[filter(temperature > 25); transform(fahrenheit, (temperature * 1.8) + 32)]", handleClimate);
Expressions
Supported Operators
| Mathematical Operators | |
|---|---|
| Operator | Description |
| + | Additive operator / Unary plus |
| - | Subtraction operator / Unary minus |
| * | Multiplication operator, can be omitted in front of an open bracket |
| / | Division operator |
| % | Remainder operator (Modulo) |
| ^ | Power operator |
| Boolean Operators* | |
|---|---|
| Operator | Description |
| = | Equals |
| == | Equals |
| != | Not equals |
| <> | Not equals |
| < | Less than |
| <= | Less than or equal to |
| > | Greater than |
| >= | Greater than or equal to |
| && | Boolean and |
| || | Boolean or |
*Boolean operators result always in a BigDecimal value of 1 or 0 (zero). Any non-zero value is treated as a true value. Boolean not is implemented by a function.
Supported Functions
| Function* | Description |
|---|---|
| NOT(expression) | Boolean negation, 1 (means true) if the expression is not zero |
| IF(condition,value_if_true,value_if_false) | Returns one value if the condition evaluates to true or the other if it evaluates to false |
| RANDOM() | Produces a random number between 0 and 1 |
| MIN(e1,e2, ...) | Returns the smallest of the given expressions |
| MAX(e1,e2, ...) | Returns the biggest of the given expressions |
| ABS(expression) | Returns the absolute (non-negative) value of the expression |
| ROUND(expression,precision) | Rounds a value to a certain number of digits, uses the current rounding mode |
| FLOOR(expression) | Rounds the value down to the nearest integer |
| CEILING(expression) | Rounds the value up to the nearest integer |
| LOG(expression) | Returns the natural logarithm (base e) of an expression |
| LOG10(expression) | Returns the common logarithm (base 10) of an expression |
| SQRT(expression) | Returns the square root of an expression |
| SIN(expression) | Returns the trigonometric sine of an angle (in degrees) |
| COS(expression) | Returns the trigonometric cosine of an angle (in degrees) |
| TAN(expression) | Returns the trigonometric tangens of an angle (in degrees) |
| COT(expression) | Returns the trigonometric cotangens of an angle (in degrees) |
| ASIN(expression) | Returns the angle of asin (in degrees) |
| ACOS(expression) | Returns the angle of acos (in degrees) |
| ATAN(expression) | Returns the angle of atan (in degrees) |
| ACOT(expression) | Returns the angle of acot (in degrees) |
| ATAN2(y,x) | Returns the angle of atan2 (in degrees) |
| SINH(expression) | Returns the hyperbolic sine of a value |
| COSH(expression) | Returns the hyperbolic cosine of a value |
| TANH(expression) | Returns the hyperbolic tangens of a value |
| COTH(expression) | Returns the hyperbolic cotangens of a value |
| SEC(expression) | Returns the secant (in degrees) |
| CSC(expression) | Returns the cosecant (in degrees) |
| SECH(expression) | Returns the hyperbolic secant (in degrees) |
| CSCH(expression) | Returns the hyperbolic cosecant (in degrees) |
| ASINH(expression) | Returns the angle of hyperbolic sine (in degrees) |
| ACOSH(expression) | Returns the angle of hyperbolic cosine (in degrees) |
| ATANH(expression) | Returns the angle of hyperbolic tangens of a value |
| RAD(expression) | Converts an angle measured in degrees to an approximately equivalent angle measured in radians |
| DEG(expression) | Converts an angle measured in radians to an approximately equivalent angle measured in degrees |
| FACT(expression) | Note: Factorial is disabled on the OOCSI server for security. |
*Functions names are case insensitive.
Supported Constants
| Constant | Description |
|---|---|
| e | The value of e, exact to 70 digits |
| PI | The value of PI, exact to 100 digits |
| TRUE | The value one |
| FALSE | The value zero |
| NULL | The null value |
Rolling Window Functions
When working with sensor streams or interactive input, you frequently need to calculate rolling averages, sums, or peak values over time without maintaining client-side buffers. The OOCSI server provides five built-in sliding window aggregation functions:
| Function | Description |
|---|---|
mean(variable, n) | Calculates the arithmetic mean (average) of variable over the last n events. |
sum(variable, n) | Calculates the running sum of variable over the last n events. |
stdev(variable, n) | Calculates the standard deviation of variable over the last n events. |
emin(variable, n) | Calculates the minimum value of variable over the last n events. |
emax(variable, n) | Calculates the maximum value of variable over the last n events. |
Important notes on window functions:
- Window size cap: The window length
nis automatically capped at 50 events (n <= 50).eminandemax: To calculate running minimum and maximum over a window of n events, useeminandemax. This distinguishes them from the standard mathematical comparison functionsMIN(a, b)andMAX(a, b).
Examples in all supported languages
Smooth out a noisy light sensor by computing a 10-sample rolling average directly on the server:
// Java / Processing
oocsi.subscribe("sensors/light[transform(smooth_lux, mean(lux, 10))]", "handleLight");
# Python
oocsi.subscribe("sensors/light[transform(smooth_lux, mean(lux, 10))]", handle_light)
// JavaScript (oocsi-web.js)
OOCSI.subscribe("sensors/light[transform(smooth_lux, mean(lux, 10))]", (msg) => {
console.log("Smoothed Lux:", msg.data.smooth_lux);
});
// ESP32 (Embedded C++ / Arduino)
oocsi.subscribe("sensors/light[transform(smooth_lux, mean(lux, 10))]", handleLight);
Filter and Transform Intricacies
flowchart TD
IN["Incoming Message on Channel"] --> TEST_FILTERS{"Evaluate filter(...)<br/>Expressions"}
TEST_FILTERS -->|Any expression FALSE or key missing| DROP["Discard Message<br/>(No delivery, save bandwidth)"]
TEST_FILTERS -->|All expressions TRUE| TRANS{"Any transform(...)<br/>Expressions?"}
TRANS -->|No| DELIVER["Deliver Original Message<br/>to Client"]
TRANS -->|Yes| RUN_TRANS["Execute Transforms in Sequence<br/>(Missing keys default to 0)"]
RUN_TRANS --> WIN{"Rolling Window Function?<br/>(mean, sum, stdev, emin, emax)"}
WIN -->|Yes| EVAL_WIN["Update Circular Buffer (cap 50)<br/>Compute Window Value"]
WIN -->|No| APPLY_TRANS["Update/Add Payload Keys"]
EVAL_WIN --> APPLY_TRANS
APPLY_TRANS --> DELIVER_TRANS["Deliver Transformed Message<br/>to Client"]
When designing advanced interactions with server-side filters and transforms, keep the following execution details in mind:
1. Order of Execution
When you combine multiple filters and transformations separated by ;:
- Filters run first: The server tests all
filter(...)expressions. If any filter evaluates tofalse, the message is immediately dropped. Transformations will not be computed, and no message is delivered to your client. - Transforms run in sequence: If all filters pass, each
transform(key, expression)is evaluated in the order specified. Each transformation adds or updates the specified key in the transformed message payload.
2. Handling Missing Keys
Messages on a channel do not always contain the same set of keys. The server handles missing keys differently depending on whether they occur in a filter or a transform:
- In
filter(...): If an expression references a key that is missing from the incoming message, the filter evaluation aborts safely and the message is discarded. - In
transform(...): If an expression references a missing key, the server does not discard the message; instead, the missing key safely defaults to0.
3. Evaluation Safety and Error Handling
If an expression produces a runtime error (such as division by zero or invalid type conversion), the server catches the exception safely and drops the message. Your client and the server remain stable and unaffected.
4. Combining with Password-Protected Channels
You can apply filters and transforms to private, password-protected channels by specifying the password before the bracketed expressions:
// Java / Processing
oocsi.subscribe("secret_lab:myPass123[filter(temp > 30); transform(temp_f, (temp * 1.8) + 32)]", "handleAlert");
# Python
oocsi.subscribe("secret_lab:myPass123[filter(temp > 30); transform(temp_f, (temp * 1.8) + 32)]", handle_alert)
// JavaScript (oocsi-web.js)
OOCSI.subscribe("secret_lab:myPass123[filter(temp > 30); transform(temp_f, (temp * 1.8) + 32)]", (msg) => {
console.log("High temp alert:", msg.data);
});
// ESP32 (Embedded C++ / Arduino)
oocsi.subscribe("secret_lab:myPass123[filter(temp > 30); transform(temp_f, (temp * 1.8) + 32)]", handleAlert);
5. Security Restrictions and Syntax Constraints
- Length limit: The function string inside
[...]can be at most 512 characters. - Balanced brackets: Brackets and parentheses must be properly closed and balanced.
- Blocked functions: To protect server resources and prevent denial-of-service, potentially dangerous or unbounded functions in the underlying expression engine (such as
FACT,STR_FORMAT,STR_MATCHES,DT_DATE_NEW, andDT_DURATION_NEW) are blocked.
Credits
All expressions are evaluated with the EvalEx library, with custom extensions and security hardening for OOCSI.