Modular ESP32 automation platform with stackable open-source modules, built-in BASIC runtime, distributed BIR networking, local dashboards (optional cloud), and HTTP API for Python and AI control.
BUGITRUN_SETUP.http://192.168.4.1 and enter your Wi-Fi + Auth Key + time zone.Every automation stems from the same principles. After ten years of developing hardware for wireless networks and cloud infrastructures, one thing is clear: whether it’s a small device or an extensive network of units, the overhead remains the same. Bringing up the basics, setup, and debugging. These are hours of work you must sacrifice before you even start creating real value.
Bugitrun was built to change that. It's a modular platform designed to take these repetitive steps off your hands. Fast setup with no unnecessary obstacles. A clear structure that makes sense at first glance. The freedom to focus on what you're truly building. For beginners and seasoned developers alike. For anyone who wants to spend less time on configuration – and more time creating.
Bugitrun is a platform designed for beginners, students, schools, and advanced users who build wireless control and sensor monitoring systems within a single internal network. It offers a simple and intuitive way to operate and manage projects – without complications, without unnecessary complexity. The platform is built on an open and practically unlimited architecture. A modular approach, automatic detection of connected modules, and cooperation between multiple units within the same network form the foundation on which you can build projects of any scale.
Thanks to its open source code and modular architecture, Bugitrun adapts to your needs. It offers a clear dashboard accessible from your local network, as well as an experimental cloud interface with encrypted communication for secure remote work. It is an flexible system that grows together with your project.
At the core of Bugitrun is the BIR module.
Each BIR includes an integrated test set of inputs and outputs for quick familiarization.
It measures temperatures, switches outputs, drives servos,
and keeps track of the system state.
You monitor everything through a local or cloud dashboard in your browser:
you see live values, can intervene manually, adjust settings,
and write automation logic while the program itself runs directly inside the BIR.
When that’s not enough, BIR grows with your project.
Its main strength is a 3D spatially oriented bus for connecting external modules – the ecosystem.
These modules are open source, and the community can expand the system
practically without limits with hundreds of variants for different use cases:
relay boards, input blocks, special sensors, and more.
Each module announces itself when connected, gets its own objects and tile color,
and is ready to use instantly.
You don’t rewrite firmware – you just add hardware
and use the new objects directly in your logic.
Everything is tied together by a simple BASIC-like language running on every BIR.
Your program reads sensor values, sets outputs, calls BIRNET functions,
and keeps running even if you close the dashboard or the internet goes down.
Just a few readable lines are enough to turn a set of sensors and modules
into a system that runs on its own – exactly the way you designed it.
Multiple BIR modules can cooperate over the network using BIRNET.
A BIR in the boiler room can share data with another in the greenhouse,
or a “server” BIR can collect values from several rooms
and make global decisions.
Technically it’s a simple small HTTP API,
but from the user’s point of view the system behaves like a single distributed whole,
where individual BIRs can see selected values from the others.
On top of BIRNET sharing values between modules, BIR devices can also be driven externally from Python via the same lightweight HTTP interface. That makes it easy to build custom automations, experiments, and higher-level logic on a PC or server — from simple scripts that read sensors and set outputs, up to more advanced control like neural networks that learn from live data and drive actuators in real time.
Initialization is used to configure the BIR module’s Wi-Fi connection, Auth Key and time zone. Without a completed initialization, the module does not enter normal operating mode.
If the BIR module starts without stored configuration, it automatically enters initialization mode:
The initialization procedure is as follows:
The module stores the provided data, connects to the configured Wi-Fi network and attempts to complete the initialization with the cloud backend.
After the initialization attempt, the result is indicated using the status LED:
If initialization fails (the LED is blinking fast), the BIR module does not restart automatically and must be restarted by briefly disconnecting and reconnecting the power supply. Then try to start the initialization again by holding the button for 10 seconds.
Initialization can be repeated at any time, for example when changing Wi-Fi parameters or the Auth Key, or when handing the module over to a new user. To restart initialization, press and hold the setup button on the BIR module for approximately 10 seconds, until the status LED turns on. The device will erase its stored configuration, reboot, create the BUGITRUN_SETUP Wi-Fi network again and expose the initialization page at http://192.168.4.1, so the initial initialization procedure can be performed once more.
Both the Local and Cloud Dashboards share a common set of core features to ensure a consistent experience.
Click the module title in the top bar to rename the BIR module. If the name is left empty, the dashboards show only the connection details (IP, MAC, and firmware version). The name is stored inside the BIR module, so it persists after restart and stays consistent across both the Local and Cloud Dashboards. If the module is renamed from another device or dashboard, the title updates automatically on the next refresh (Cloud) or within the next polling cycle (Local).
Both dashboards display sensors and outputs as tiles, each identified by:
System tiles such as TIM, DTE, DAY, OUT, INP, TMP, VAL use predefined colors. For external modules, the dashboard generates a unique tile color based on the module’s three-letter ID.
Internally, each tile corresponds to an object with a three-letter prefix and a numeric index. The core BIR module provides built-in objects OUT_0–OUT_2 (5 V outputs), INP_3 (digital input), SRV_4 and SRV_5 (servo outputs 0–180), followed by optional DS18B20 sensors TMP_6… and channels of external modules such as RLY_x or ADC_x. Numbering is always continuous and any remaining indices are filled with generic VAL_x objects. The total range of tiles is fixed to either 0–44 (no external modules present) or 0–76 (at least one external module detected).
You can rename any object on both dashboards by clicking its name. When choosing a name, it must be unique, must not clash with BASIC keywords, be at most 12 characters long, and may contain only letters, digits, the underscore (_) and the hyphen (-). The user-defined name can then be used directly as an identifier in BASIC code, so you don’t have to reference objects only by their three-letter IDs.
For outputs (e.g. relays), you can click on the value and enter a new float value, which is then sent to the device.
Both dashboards provide a BASIC-like editor where you write your program. Once uploaded, the program runs directly on the BIR module and continues operating even when the device is offline.
Both dashboards allow you to:
This makes it easy to move projects between the Local and Cloud Dashboards or between different BIR modules.
Each dashboard includes an Erase button with two functions:
The Cloud Dashboard runs on the Bugitrun server and is accessed securely over HTTPS using your private access key. It provides a complete remote interface for monitoring and controlling the BIR module from anywhere.
After logging in, you are presented with a live grid of tiles. Each tile represents a single object and updates automatically as new data arrives from the module. Tile colors indicate the object’s module type.
Below the grid is the BASIC editor – a clean editing area for writing and modifying your program. Under the editor, the following project-management controls are available:
Uploaded programs start immediately and continue running even when the device is offline. All Cloud Dashboard projects are stored directly on the server, allowing access from any location.
The upper-right corner menu provides these actions:
Switches to the Local Dashboard hosted directly on the BIR module. This option is available only when your browser is on the same local network as the device.
Shows or hides the graph panel. The graph runs entirely inside the browser and stores all data locally in the browser’s memory. Closing or refreshing the page clears the graph. This design provides high performance at scale without any server-side storage.
The graph panel allows you to:
Export and import allow you to share or transfer projects as files directly between devices. You can move a project between multiple BIR modules or between the Cloud and Local Dashboard simply by exporting it to a file and importing it where you need it.
Clears the dashboard state and prepares it for fresh synchronization with the device.
Performs a secure remote firmware update of the BIR module. The update is protected by a cryptographic digital signature (ECDSA). Updates are applied remotely and do not require physical access to the device.
Cycles through the available visual skins. Skins rotate in sequence, and additional designs may be added over time.
The Cloud Dashboard includes a global one-session lock.
This mechanism prevents race conditions and ensures that commands sent from the cloud remain consistent and secure.
The Cloud Dashboard is ideal for remotely supervising and controlling the BIR module – monitoring sensors, managing programs, performing secure firmware updates, sharing projects between devices, or analyzing data over time. In cloud mode, the dashboard shows updated values from the BIR module roughly every 2 seconds, while the Local Dashboard uses a shorter 0.5-second refresh interval for faster, more immediate feedback when you are on the same network.
The Local Dashboard runs directly on the BIR module at its local IP address within your network. For reliable access, you are encouraged to reserve this IP address in your Wi-Fi router. There is no cloud layer involved, which means:
The main area displays a live grid of tiles:
Below the grid, the Local Dashboard provides a BASIC code editor for writing and modifying programs. The editor includes:
In addition, the Local Dashboard supports system-level commands directly inside the BASIC editor:
SYSTEM(COMMAND)SYSTEM() instruction is executed immediately and directly on the BIR module.Command description:
SYSTEM(ENABLE_INSECURE_OTA_ONCE) – enables a one-time emergency OTA update without certificate validation.
Other SYSTEM() commands may be used in the same way.
A small floating menu provides essential local tools:
The Local Dashboard enforces a single active session only within the same browser environment.
Although this is allowed, it is strongly discouraged to run the Local Dashboard on multiple devices or browsers simultaneously, because it can:
For stable operation, the Local Dashboard should be used from a single browser instance at a time. An in-browser overlay prevents parallel sessions within the same browser, but it does not block external devices — therefore the responsibility is on the user to avoid multiple local controllers.
The Local Dashboard is ideal for configuration, testing, development, and emergency maintenance, especially when you need fast, low-latency feedback directly from the device. With its 0.5-second refresh interval (compared to 2 seconds on the Cloud Dashboard), it gives you a much faster visual response when your BASIC program changes values or states. The BASIC program itself always runs directly on the BIR module and is not affected by the dashboard refresh interval. The BASIC interpreter runs offline on the BIR module with a minimum step interval of 10 ms for internal system I/O and 100 ms for external modules.
The BIR module is built around an ESP32-WROOM-N4 with a dual-core CPU. It exposes a fixed set of built-in hardware channels that are always present and directly accessible from BASIC. All values are stored internally as floating-point numbers, even if they represent digital states.
The BIR module is powered via a USB-C connector. This connector is used for power only and does not provide a data interface. The 5 V rail is also distributed to the external module bus to supply connected expansion modules. Always choose a USB-C power adapter with respect to the connected external modules and their maximum current consumption.
TMP_6 … TMP_15
(up to 10 sensors in total).TMP_x ID.TMP_x objects are created.TIM_SYS – current time of day (same numeric representation as TIME).DTE_SYS – current date (same numeric representation as DATE).DAY_SYS – current day of week (same numeric representation as DAY).These objects are updated automatically by the BIR firmware and are read-only from BASIC.
All remaining IDs up to the current maximum tile index are exposed as VAL_x objects.
They are pure numeric slots stored in the BIR module and are not tied to any specific hardware.
They are intended as general-purpose variables that survive restarts and can be used in any BASIC program.
If no external modules are connected, the dashboards show tiles from index 0 up to 44
(OUT / INP / SRV / TMP and then a block of VAL_x objects).
If at least one external module is detected, the available tile range is extended to 0–76.
The free space between the last hardware ID and the maximum index is always filled with VAL_x.
The built-in BIR hardware (OUT / INP / SRV / TMP / VAL) is primarily meant for learning, prototyping and smaller projects. Because these channels are connected directly to ESP32 pins, the internal response is very fast and latency is minimal.
The built-in Control LED indicates Wi-Fi connectivity and dashboard activity. While the BIR module is not connected to Wi-Fi, the LED blinks until the connection is established. Once Wi-Fi is online, the LED stays on and briefly turns off as a short pulse whenever the Local Dashboard is polling or when the Cloud Dashboard performs an update.
BIRNET allows multiple BIR modules to communicate with each other directly over the local network, without using the cloud backend. Typical use cases include sharing sensor values between rooms, synchronizing outputs across devices, or offloading part of the logic to a dedicated “server” BIR.
BIRNET is implemented as a small HTTP-based API between BIR modules. Each BIR exposes simple LAN endpoints and the BASIC interpreter provides high-level commands that hide the HTTP details and handle timeouts internally.
The simplest form of BIRNET is reading a single value from another BIR using the GET() function.
It is used directly inside BASIC expressions:
0 WAITINIT
10 LET LED = GET("192.168.0.200","OUT_0", 500)
20 LET VAL_44 = BIRNETSTAT
30 GOTO 10
Parameters:
OUT_0, TMP_1, VAL_10),
If the timeout is omitted, it defaults to 1000 ms. The timeout is internally clamped
to the range 300–5000 ms. Even if the server responds faster, the GET() command
blocks the BASIC interpreter for the full timeout duration – this guarantees that there is no internal queue
of pending TCP requests and prevents network overload when the remote module is offline or restarting.
On the server side, the request is handled by a lightweight /peer endpoint. If the target BIR
is not yet initialized (for example, still waiting for sensors to come online), it returns a temporary
NOT_READY state and the client simply finishes the GET() without updating any value.
BIRNET SINGLE demo:
OUT_0 on the server module.OUT_0 from the server and mirrors the LED.
For more advanced scenarios, BIRNET provides a multi-value synchronization mechanism. Instead of calling
GET() multiple times, the client can fetch several values from the server in one step and
update multiple local objects atomically.
The sequence is always:
Configures the target BIR and timeout for all subsequent BIRNETSYNC calls:
BIRNETSET("192.168.0.200", 300)
Defines which remote IDs will be read from the server. Up to 10 IDs are supported:
SERVERIDS(OUT_0, OUT_1, OUT_2)
Defines which local IDs will be overwritten by the values from the server. The number and order of local IDs must match the remote IDs:
CLIENTIDS(OUT_0, OUT_1, OUT_2)
After this configuration, the first remote ID maps to the first local ID, the second to the second, etc.
Executes one synchronization step:
0 WAITINIT
10 BIRNETSET("192.168.0.200", 300)
20 SERVERIDS(OUT_0, OUT_1, OUT_2)
30 CLIENTIDS(OUT_0, OUT_1, OUT_2)
40 BIRNETSYNC
50 LET VAL_44 = BIRNETSTAT
60 GOTO 40
Internally, the client sends a single request to the server (endpoint /peer_multi)
and receives all requested values at once. The most important property is
all-or-nothing behavior:
CLIENTIDS are updated in one step.This makes multi-sync safe for professional automation scenarios where partial updates could cause inconsistent states or unwanted behavior.
BIRNET MULTI demo:
BIRNETSTAT is a read-only numeric value that always contains the result of the last
BIRNET operation (GET(...) or BIRNETSYNC). It can be used anywhere a number
is expected (for example in LET or IF).
The value is set automatically to one of the following codes:
0 - no BIRNET request has finished yet (initial state after boot or reset)
200 - last BIRNET request succeeded and the remote module returned valid data
-1 - connection error (remote BIR could not be reached – Wi-Fi not connected or HTTP connection failed)
-2 - remote BIR responded, but the response was empty or invalid
400 - invalid request (wrong or missing ID in the command)
404 - requested ID does not exist on the remote BIR
503 - remote BIR is not ready yet (for example its sensors or external modules are still initializing)
GET() and BIRNETSYNC are blocking operations: they pause the BASIC interpreter
for the configured timeout on each call.SERVERIDS – an unknown ID on the server side causes the entire sync
to be skipped, by design, to keep your client state consistent.The Ecosystem represents a set of highly compatible expansion modules for sensors, inputs, outputs, and basically any “object” you can imagine around an MCU-based system. Each external module does not deal with any user interaction — it is purely hardware that serves a specific sensor or function and exposes its values/channels to the BIR module.
Every module has its own 3-letter ID made of uppercase letters, for example RLY.
This ID also determines the tile color generated on the dashboards according to the internal color logic.
In the module firmware, you typically define the module ID, number of channels, and the module type
(input / output). Each external module provides a defined number of channels of the same type.
The channel count is determined by the module’s hardware and firmware, with a maximum of 8 channels per module.
Up to 8 external modules can be connected to the expansion bus, resulting in up to 64 objects available to a single BIR module.
DS18B20 sensors are not counted as external modules. Up to 10 DS18B20 sensors can be connected directly to the BIR module. These sensors are not plug-and-play: if one stops communicating, the BIR module will only show an error on the corresponding tile, and whenever DS18B20 sensors are added or removed, the BIR module must be restarted. This does not apply to external modules, which are plug-and-play. They can be connected and disconnected while the system is running, and the BIR module will automatically detect the change and add or remove the modules internally and in the dashboards.
The Ecosystem is open source — anyone can design and build their own modules. This manual section describes the default reference wiring, including a reference PCB design and a basic firmware template using the Microchip PIC18F15Q40 as the module MCU.
The reference schematic and PCB were created in DesignSpark PCB 12.0. The reference firmware is built in MPLAB X using the XC8 compiler v3.00 (pack 1.27.449).
However, you can use any MCU as long as it supports UART and SPI communication.
The LET instruction assigns the result of a numeric expression to a target NAME.
All values in BIR BASIC are stored as floating-point numbers.
LET NAME = numeric_expression
OUT_0, OUT_1, OUT_2, INP_3, SRV_4, SRV_5, TMP_6, module IDs such as RLY_7, ADC_11, or generic value objects VAL_x up to the current hardware limit), orheater, room_temp).AVG_1, DEL_123, MIX_42.
IF, THEN, TIME, DATE, DAY, ABS, RND, PI, BIRNETSTAT, …).AND, OR, NOT) are not allowed inside numeric expressions.
They are reserved for boolean expressions in IF conditions.TMP_6, OUT_0, VAL_20, TIM_SYS) or their user-defined names (e.g. heater, room_temp),AAA_123 pattern (e.g. AVG_1, MIX_1, DEL_10),+, -, *, /,+ and -,( ... ),ABS, MAX, MIN, SIN, COS, TAN, RND, LOG, LN, EXP, ROUND, FLOOR, CEIL, SQRT, …,PI, E, BIRNETSTAT.LET cannot contain an IF expression – conditions belong in a separate
IF ... THEN ... line.GET() can be used in LET only as the whole right-hand side, e.g.
LET X = GET(...).
It cannot be combined inside another expression like 1 + GET(...).Simple assignment using system IDs:
10 LET OUT_0 = 1 # turn relay ON
20 LET VAL_20 = TMP_6 # copy current temperature into VAL_20
Assignment using user-defined names:
10 LET heater = 1 # 'heater' is a user name mapped to OUT_0
20 LET room_temp = TMP_6 # mix of user name and system ID
30 LET LED = 1 - LED # toggle LED (LED is an existing object name)
Using hidden standalone variables (AAA_123 pattern):
10 LET AVG_1 = (TMP_6 + TMP_7) / 2 # hidden variable AVG_1
20 LET DEL_1 = ABS(AVG_1 - 21) # reuse AVG_1 in another expression
30 LET MIX_1 = SIN(PI / 2) * 10 # another hidden variable
Using built-in functions and parentheses in a more complex expression:
10 LET MIX_1 = ( MAX(TMP_6, TMP_7) + ABS(VAL_20 - 3.5) ) / 2
20 LET CMD_1 = ROUND( MIX_1 / 5 )
30 LET GRF_1 = RND(50) + LOG( TMP_6 + 10 )
Using GET() inside LET (only as the whole expression):
10 LET heater = GET("192.168.0.200","OUT_0", 500)
20 LET STA_1 = BIRNETSTAT # store last BIRNET status code into a hidden variable
The IF instruction evaluates a boolean expression and, depending on the result, executes
one of two possible actions:
IF boolean_expression THEN single_command
IF boolean_expression THEN single_command ELSE single_command
After THEN and ELSE you can use:
LET ...DELAY ...GOTO ...GOSUB ...RETURNPRINT ...IF ... THEN ... without its own ELSE.
Other BASIC commands (such as WAITINIT, WAITCLOUDTICK, BIRNETSET, SERVERIDS, CLIENTIDS, BIRNETSYNC, FOR, NEXT, …) must be placed on their own line – they cannot be used directly after THEN or ELSE.
Boolean expressions in IF support:
LET),Comparison operators:
==
=
!=
<
>
<=
>=
Each side of the comparison is a numeric expression, for example:
TMP_6 > 21(TMP_6 + TMP_7) / 2 <= 25ABS(room_temp - 21) < 0.5BIRNETSTAT = 200Logical operators:
ANDORNOT
Operators AND, OR and NOT are written as words (not && or ||)
and are case-insensitive (and, Or, NOT all work).
Precedence:
NOT has the highest priority,AND,OR.
Parentheses ( ... ) can be used to override the default precedence.
Truth value without an explicit comparison:
If no comparison operator is found (for example IF heater THEN ...),
the numeric expression is evaluated and treated as:
THEN is executed.ELSE part is present, the command after ELSE is executed.ELSE, the line does nothing.
Only one command is allowed in each branch – there is no support for multiple commands separated by : or similar.
If you need multiple actions, use GOTO or GOSUB to jump to a block of code on separate lines.
Simple threshold:
10 IF room_temp < 21 THEN LET heater = 1
20 IF room_temp > 23 THEN LET heater = 0
Range check with ELSE:
10 IF TMP_6 >= 18 AND TMP_6 <= 24 THEN LET comfort = 1 ELSE LET comfort = 0
Boolean-style numeric expression:
10 IF heater THEN LET LED = 1 ELSE LET LED = 0 # 'heater' is true when its value != 0
Nested IF (safe form):
10 IF TMP_6 > 25 THEN IF window = 0 THEN LET heater = 0
In this form, the inner IF is the single command after THEN.
The inner IF should not use its own ELSE on the same line – any ELSE always belongs
to the first IF on the line. For more complex conditions, use logical operators
(AND, OR, NOT) or split the logic into multiple lines.
Example program (IF THEN ELSE DEMO):
# =================
# IF THEN ELSE DEMO
# =================
10 LET SWITCH = 1
20 IF (VALUE = 32 OR VALUE = 64 OR VALUE = 128) AND SWITCH = 1 THEN LET LED1 = 1 ELSE LET LED1 = 0
30 IF (VALUE = 10 OR VALUE = 20 OR VALUE = 30) AND SWITCH = 1 THEN LET LED2 = 1 ELSE LET LED2 = 0
40 IF (VALUE = 11 OR VALUE = 22 OR VALUE = 33) AND SWITCH = 1 THEN LET LED3 = 1 ELSE LET LED3 = 0
50 DELAY 100
60 GOTO 20
The GOTO instruction jumps to another line in the program.
The target is specified by its line number – the number at the beginning of a BASIC line.
GOTO lineNumber
10, 100, 250).GOTO targets point to existing lines before the program is uploaded.
If a GOTO to a non-existing line somehow reaches the interpreter, it is ignored at runtime and execution continues with the next line.GOTO can be used:
20 GOTO 100),THEN or ELSE in an IF line (e.g. 10 IF VALUE > 0 THEN GOTO 100).: or similar.# ===============
# IF AS CASE DEMO
# ===============
# Replace select case statement
0 LET SPEED = 100
10 LET VALUE = RND()
20 DELAY SPEED
30 IF VALUE >= 0.0 AND VALUE < 0.2 THEN GOTO 1000
40 IF VALUE >= 0.2 AND VALUE < 0.4 THEN GOTO 2000
50 IF VALUE >= 0.4 AND VALUE < 0.6 THEN GOTO 3000
60 GOTO 10
1000 LET LED1 = 1 - LED1
1010 GOTO 10
2000 LET LED2 = 1 - LED2
2010 GOTO 10
3000 LET LED3 = 1 - LED3
3010 GOTO 10
GOSUB is used to call a subroutine located at another line number.
The interpreter remembers where it came from, jumps to the subroutine, and later
RETURN brings execution back to the line after the last GOSUB.
GOSUB lineNumber
RETURN
GOSUB targets before upload and reports
calls to missing lines as errors.GOSUB is executed:
GOSUB calls).GOSUB calls no longer enter the subroutine
(the call is effectively ignored and execution stays in the main flow).GOSUB can also be used after THEN or ELSE in an IF line.RETURN should be placed at the end of a subroutine.RETURN is executed:
GOSUB is taken from the stack,GOSUB.RETURN is executed when no GOSUB is active, it has no effect and
execution simply continues with the next line.RETURN, it continues
into the following lines. For clarity, it is recommended to always end subroutines with an explicit
RETURN.10 GOSUB 100
20 DELAY 1000
30 GOTO 10
100 LET LED = 1
110 DELAY 200
120 LET LED = 0
130 RETURN
10 IF TMP_6 > 25 THEN GOSUB 100
20 GOTO 10
100 LET heater = 1
110 RETURN
In the second example, the subroutine at line 100 is called only when
the condition in line 10 is true. After RETURN, execution continues at line 20.
# =========
# GOSUB DEMO
# =========
10 LET LED1 = 1 # LEVEL 0
20 GOSUB 100
30 DELAY 500
40 LET LED1 = 0
50 DELAY 500
60 GOTO 10
100 IF LEVEL == 0 THEN RETURN # LEVEL 1
110 LET LED2 = 1
120 IF LEVEL == 2 THEN GOSUB 200
130 DELAY 500
140 LET LED2 = 0
150 DELAY 500
160 GOTO 100
200 LET LED3 = 1 # LEVEL 2
210 DELAY 500
220 LET LED3 = 0
230 DELAY 500
240 IF LEVEL == 1 THEN RETURN
250 GOTO 200
The FOR loop repeats a block of code a given number of times.
The loop variable is numeric and is updated automatically on each NEXT.
FOR var = start TO end
FOR var = start TO end STEP step_value
...
NEXT
NEXT var
VAL_10, OUT_0, TMP_6),LOOP, SPEED, INC1),AAA_123 pattern (e.g. LOP_1).STEP 1 is used by default,STEP is allowed (counting down),STEP is not recommended, because it makes the loop effectively run only once.FOR must have a matching NEXT.
Nested loops are supported and must be properly paired in the source code.NEXT can optionally specify the loop variable
(e.g. NEXT LOOP). If present, the name must match the corresponding FOR.
If omitted, the nearest active FOR is used.LET, IF, DELAY, GOSUB, PRINT, etc.).Simple loop 0–9:
10 FOR LOOP = 0 TO 9
20 LET OUT_0 = 1
30 DELAY 200
40 LET OUT_0 = 0
50 DELAY 200
60 NEXT LOOP
Loop with STEP and a helper variable:
10 FOR LOOP = 0 TO 20 STEP 0.5
20 LET INVERSE = 20 - LOOP
30 LET LED = 1
40 DELAY LOOP
50 LET LED = 0
60 DELAY INVERSE
70 NEXT LOOP
# ==================
# FOR NEXT STEP DEMO
# ==================
0 LET CYCLES = 0
1 LET FAST_STEP = 5
10 FOR INC1 = 2.5 TO -2.5 STEP -0.1
20 FOR INC2 = 0 TO 200 STEP FAST_STEP
30 LET LED1 = 1
40 DELAY 50
50 LET LED1 = 0
60 DELAY 50
70 NEXT INC2
80 LET FLAG = FLAG + 1.5
90 DELAY 1000
100 NEXT INC1
110 LET LED1 = 1
120 LET FLAG = 0
130 DELAY 10000
140 LET CYCLES = CYCLES + 1
150 GOTO 10
The DELAY instruction pauses program execution for a specified number of milliseconds.
DELAY ms_expression
DELAY uses a time anchor to keep the loop period stable:
successive DELAY calls with the same value create a regular timing pattern
(useful in main loops).WAITCLOUDTICK, the next DELAY automatically re-synchronizes
its internal time anchor to the current moment, so timing continues smoothly from the cloud tick.DELAY the BASIC interpreter task is paused and does not execute other BASIC lines,
but the BIR firmware and other internal tasks continue running normally.10 LET MS = 200
20 LET LED = 1
30 DELAY MS
40 LET LED = 0
50 DELAY MS
60 GOTO 20
The WAITINIT instruction waits until the module finishes initialization
and all connected sensors (DS18B20 and external modules) have their first valid readings.
WAITINIT
WAITINIT takes no arguments.0), but it can be used on any line.
If initialization is already finished, WAITINIT returns immediately.WAITINIT line and sleeps for short intervals, so the rest of the program does not run yet.WAITINIT, the BASIC program starts immediately after boot and may use
initial sensor values that are still zero or otherwise invalid.WAITINIT must be used as a standalone command on its own line.
It cannot be placed after THEN or ELSE in an IF statement.# =============
# WAITINIT demo
# =============
# For this demo, at least one DS18B20 sensor is required.
# The WAITINIT command waits for the initialization to complete
# and for the first valid readings from all DS18B20 and external sensors.
0 WAITINIT
1 LET FIRST_TEMP = TEMP
10 LET LED = 1 - LED
20 DELAY 500
30 GOTO 10
The WAITCLOUDTICK instruction blocks the BASIC program until the cloud server
signals a new synchronization tick (when the online dashboard is active).
WAITCLOUDTICK
WAITCLOUDTICK takes no arguments.WAITCLOUDTICK until the next cloud tick arrives.
The BASIC interpreter repeatedly sleeps in short intervals while it is waiting.WAITCLOUDTICK unblocks, the internal timing anchor
for DELAY is re-synchronized, and execution continues on the next line.WAITCLOUDTICK line until a client reconnects.WAITCLOUDTICK must be used as a standalone command on its own line.
It cannot be placed after THEN or ELSE in an IF statement.# ====================
# WAIT CLOUD TICK demo
# ====================
# Blinks the LED for 0.5 s, then waits for the next cloud tick.
# WAITCLOUDTICK blocks until the server cycle signals a new tick.
# Each tick increments CLOUD-TICKS to track successful cloud syncs.
# If the online dashboard is closed, the BIR module detects the client is offline
# and parks the BASIC program on line 40 (WAITCLOUDTICK).
10 LET LED = 1
20 DELAY 500
30 LET LED = 0
40 WAITCLOUDTICK
50 LET CLOUD-TICKS = CLOUD-TICKS + 1
60 GOTO 10
The PRINT instruction attaches a short text label (a "mask") to a numeric value
for display on the dashboard. The underlying variable always keeps its real floating-point value.
PRINT "text", ID
VAL_10, TMP_6, OUT_0, Door if it is the real ID, …).""), the text mask is removed and only the numeric value is shown again.PRINT can also be used after THEN or ELSE in an IF line
as a single command.# ==========
# PRINT demo
# ==========
# This demo shows how PRINT adds a text "mask" to a numeric value.
# If the first parameter is a non-empty string (up to 10 characters), that text is displayed instead of the raw value.
# If the first parameter is an empty string (""), the text is removed and only the numeric value is shown.
# The variable itself always keeps its real floating-point value for all calculations and conditions.
10 LET Door = 1 - Door
20 IF Door = 1 THEN PRINT "Open", Door ELSE PRINT "Closed", Door
30 LET DoorValue = Door
40 LET INC = INC + 1
50 IF INC >= 5 THEN GOSUB 100
60 WAITCLOUDTICK
70 GOTO 10
# Reset string mask back to the raw numeric value
100 PRINT "", Door
110 LET INC = 0
120 RETURN
BIR BASIC provides three read-only system parameters for working with time and calendar:
TIME, DATE and DAY.
They can be used in expressions and IF conditions just like numeric values.
17.5, 6:15 as 6.25.day + month/100.
9.01, 31 December is 31.12.SUN=0, MON=1, TUE=2, WED=3, THU=4, FRI=5, SAT=6.
TIME, DATE and DAY are read-only system parameters.
They cannot be used on the left side of LET or as loop variables in FOR.TIME, DATE or DAY on the dashboard.
In IF conditions, special readable forms are supported and automatically converted
to numeric expressions by the dashboard check:
TIME >= 12:02 AND TIME < 12:03 → comparison with a time range,DATE = 9.1 → comparison with the numeric date value,DAY = FRI → comparison with the numeric day-of-week code.Note about TIME comparisons:
TIME changes every second and is stored as a floating-point value.TIME = 17:31 is not recommended,
because the internal value and the rounded literal almost never match exactly.IF TIME >= 17:31 AND TIME < 17:32 THEN ...# =============
# DATETIME demo
# =============
10 IF TIME >= 12:02 AND TIME < 12:03 THEN LET LED1 = 1 ELSE LET LED1 = 0
20 IF DATE = 9.1 THEN LET LED2 = 1 ELSE LET LED2 = 0
30 IF DAY = FRI THEN LET LED3 = 1 ELSE LET LED3 = 0
40 DELAY 1000
50 GOTO 10
BIR BASIC supports a set of built-in math functions and constants that can be used
in any numeric expression (in LET, IF, FOR, etc.).
All function names and constants are case-insensitive.
Functions:
ABS(x) – absolute valueMIN(a, b), MAX(a, b) – minimum / maximum of two valuesMOD(a, b) – remainder after division (floating-point modulus)SIGN(x) – returns -1 for negative, 0 for zero, 1 for positive valuesROUND(x), FLOOR(x), CEIL(x) – rounding, floor and ceilingSQRT(x) – square rootLOG(x) – base-10 logarithmLN(x) – natural logarithm (base E)EXP(x) – exSIN(x), COS(x), TAN(x) – trigonometric functions, x is in radiansRND() – random value in the range 0..1RND(max) – random value in the range 0..maxConstants:
PI – π (≈ 3.14159)E – Euler's number (≈ 2.71828)BIRNETSTAT – last BIRNET status code (numeric value that can be used in expressions and IF conditions).# =========
# Math DEMO
# =========
# -----------------------------------------
# BLOCK A: Basic arithmetic (std precedence)
# -----------------------------------------
1000 LET VAL_14 = 2 + 3 * 4 # 14
1010 LET VAL_15 = 10 / 2 * 3 # 15
1020 LET VAL_16 = 5 - 2 * 3 + 4 * 2 # 7
1030 LET VAL_17 = -5 + 2 * 3 # 1
1040 LET VAL_18 = --5 + -+-+3 # 8
# -----------------------------------------
# BLOCK B: Parentheses (explicit priority)
# -----------------------------------------
1100 LET VAL_19 = (5 + 3) * 2 # 16
1110 LET VAL_20 = (10 - 4) / (2 + 1) # 2
1120 LET VAL_21 = (3 + (2 * (1 + 1)))# 7
# -----------------------------------------
# BLOCK C: Math functions & constants
# -----------------------------------------
1200 LET VAL_22 = ABS(-5) # 5
1210 LET VAL_23 = SQRT(81) # 9
1220 LET VAL_24 = MIN(2, 8) # 2
1230 LET VAL_25 = MAX(2, 8) # 8
1240 LET VAL_26 = MOD(17, 5) # 2
1250 LET VAL_27 = ROUND(3.6) # 4
1260 LET VAL_28 = FLOOR(3.8) # 3
1270 LET VAL_29 = CEIL(3.2) # 4
1280 LET VAL_30 = SIGN(-42) # -1
1290 LET VAL_31 = LOG(100) # 2
1300 LET VAL_32 = LN(2.71828) # 1(≈)
1310 LET VAL_33 = EXP(2) # 7.389(≈)
1320 LET VAL_34 = SIN(PI / 2) # 1
1330 LET VAL_35 = COS(PI) # -1
1340 LET VAL_36 = TAN(PI / 4) # 1
1350 LET VAL_37 = RND() # 0..1
1360 LET VAL_38 = RND(100) # 0..100
1370 LET VAL_39 = E # 2.71828...
# -----------------------------------------
# BLOCK D: Variables combined with expressions
# -----------------------------------------
1400 LET OUT_0 = 1
1410 LET VAL_40 = 5 * OUT_0 + VAL_22 / 2 # 7.5
1420 LET VAL_41 = (VAL_40 + SQRT(VAL_23)) / 2 # 5.25
# -----------------------------------------
# BLOCK E: IF / THEN / ELSE with math
# -----------------------------------------
1500 LET VAL_41 = (VAL_40 + SQRT(VAL_23)) / 2 # z předchozího bloku
1510 IF VAL_41 > 10 THEN LET OUT_0 = 1 ELSE LET OUT_0 = 0
1520 IF ABS(SIN(PI/2) - 1) < 0.001 THEN LET OUT_1 = 1
1530 IF RND() > 0.5 THEN LET OUT_2 = 1 ELSE LET OUT_2 = 0
# -----------------------------------------
# BLOCK F: Loops and DELAY with expressions
# -----------------------------------------
1600 LET VAL_43 = 1000
1610 FOR VAL_44 = 1 TO 5 STEP 1
1620 LET OUT_1 = 1
1630 DELAY 1000 + VAL_43 / 2 # 1500 ms
1640 LET OUT_1 = 0
1650 DELAY 1000 + VAL_43 / 2 # 1500 ms
1660 NEXT VAL_44
The BIR HTTP interface allows you to interact with the module from external applications running on your PC or other devices in the same network. You can read sensor values, write commands, and use the module as a real-time control backend for your custom logic implemented in Python, JavaScript, or any language that supports HTTP requests.
This demo shows how you can use the BIR HTTP interface as a real-time control backend while running higher-level logic on your PC. The script first trains a tiny neural network on synthetic temperature samples (fast, offline, reproducible), and then switches into a live control loop where it continuously reads three temperature channels from the BIR module and writes a servo angle back to the device.
TMP_6/TMP_7/TMP_8 triplets and train a tiny NN (offline).0° / 90° / 180° from a strict “winner” rule (ties skipped)./peer_multi.SRV_4 via /command.
Instead of collecting real data first, the script generates synthetic temperature triplets inside a configurable range.
A simple “teacher” rule assigns only three target angles: 0°, 90°, or 180°
depending on which temperature is strictly the highest. If there is a tie for the maximum value, that sample is skipped.
This creates a clean supervised dataset and avoids ambiguous labels.
After training, the script enters an infinite loop:
it fetches the live temperatures, predicts a continuous servo angle, clamps it to 0..180,
and writes it back to the BIR module. The console output highlights the “winner” temperature channel and shows
the predicted angle versus the teacher reference (when there is no tie), so you can see how the NN behaves in real time.
# =============================================================================
# BIR NN DEMO (REGRESSION, 3 inputs -> 1 continuous output 0..180)
#
# Goal:
# - Inputs: TMP_6, TMP_7, TMP_8 (temperatures)
# - "Teacher" outputs ONLY 3 discrete targets: 0 / 90 / 180 degrees
# * if T6 is highest -> 180
# * if T7 is highest -> 90
# * if T8 is highest -> 0
# - If there is a tie for maximum: teacher returns None (skip training sample)
#
# Key idea:
# NN input uses temps relative to MIN (offset-invariant).
# features = [(T6-min)/DIFF_SCALE, (T7-min)/DIFF_SCALE, (T8-min)/DIFF_SCALE]
# =============================================================================
import time
import random
from typing import List, Tuple, Optional
import requests
import sys
# -----------------------------
# Configuration (edit here)
# -----------------------------
# BIR
BIR_IP = "192.168.0.202" # IP address of your BIR (edit this)
TIMEOUT_S = 0.5 # HTTP request timeout (seconds) - adjust if you have network issues
PERIOD_S = 1.0 # control loop period (seconds) - adjust as needed (e.g. 0.5s or 2s)
TMP_IDS = ["TMP_6", "TMP_7", "TMP_8"] # temperature sensor IDs (edit if your sensors have different IDs)
SERVO_ID = "SRV_4" # servo ID to control (edit if you want to use a different servo)
BASE = f"http://{BIR_IP}" # base URL for BIR API
# Synthetic training temperature ranges
TRAIN_T_MIN = 20.0 # wide range min
TRAIN_T_MAX = 30.0 # wide range max
TRAIN_Tmicro_MIN = 24.0 # micro range min (more near-ties)
TRAIN_Tmicro_MAX = 26.0 # micro range max
TRAIN_RATIO = 0.1 # probability of drawing a sample from micro range
TEMP_STEP = 0.25 # quantization step (matches sensor resolution)
# NN + training
SEED = 1234 # random seed for reproducibility (tune this or set to None for random seed)
LR = 0.001 # learning rate (tune this)
"""
# Lite model
HIDDEN = 16 # number of hidden neurons (tune this)
EPOCHS = 10 # number of training epochs (tune this)
STEPS_PER_EPOCH = 20_000 # number of training steps per epoch (tune this)
"""
# Bigger model
HIDDEN = 32
EPOCHS = 30
STEPS_PER_EPOCH = 100_000
#"""
# Feature scaling for temperature differences
DIFF_SCALE = 10.0 # scale factor for input features (tune this, e.g. to match typical temp differences)
# Console progress update
PROGRESS_EVERY = 5000 # update live line each N steps
# -----------------------------
# Terminal colors (ANSI)
# -----------------------------
C_RESET = "\033[0m"
C_GREEN = "\033[32m"
C_ORANGE = "\033[38;5;208m"
C_TMP_WIN = "\033[96m"
C_TMP_OTHER = "\033[90m"
# =============================================================================
# BIR API helpers
# =============================================================================
class BirApiError(Exception):
pass
def bir_peer_multi(node_base: str, ids: List[str], timeout_s: float = TIMEOUT_S) -> List[float]:
"""
GET /peer_multi?ids=TMP_6,TMP_7,TMP_8
Response body: "v1;v2;v3"
"""
r = requests.get(f"{node_base}/peer_multi", params={"ids": ",".join(ids)}, timeout=timeout_s)
if r.status_code == 503:
raise BirApiError("NOT_READY (503)")
if r.status_code in (400, 404):
raise BirApiError(f"{r.status_code} {r.text.strip()}")
r.raise_for_status()
parts = r.text.strip().split(";")
if len(parts) != len(ids):
raise BirApiError(f"Parse mismatch: got '{r.text.strip()}' for ids={ids}")
# Tolerate decimal comma
return [float(p.strip().replace(",", ".")) for p in parts]
def bir_set_value(node_base: str, sensor_id: str, value: float, timeout_s: float = TIMEOUT_S) -> None:
"""
POST /command
Body: cmd:value=ID,value#
"""
body = f"cmd:value={sensor_id},{value}#"
r = requests.post(f"{node_base}/command", data=body, timeout=timeout_s)
r.raise_for_status()
# =============================================================================
# Small utility helpers
# =============================================================================
def clamp(x: float, lo: float, hi: float) -> float:
return lo if x < lo else hi if x > hi else x
def lrelu(x: float, a: float = 0.05) -> float:
return x if x >= 0.0 else a * x
def dlrelu(x: float, a: float = 0.05) -> float:
return 1.0 if x >= 0.0 else a
def quantize_temp(t: float, step: float = TEMP_STEP) -> float:
"""Round to nearest multiple of TEMP_STEP (e.g. 0.25°C)."""
return round(t / step) * step
def live_line(s: str) -> None:
"""Overwrite current console line (ANSI)."""
sys.stdout.write("\r\033[2K" + s)
sys.stdout.flush()
# =============================================================================
# Teacher (stateless, tie -> None)
# =============================================================================
def teacher_target_deg(t6: float, t7: float, t8: float) -> Optional[float]:
"""
Stateless teacher:
- if there is a UNIQUE winner (strictly highest), returns 180/90/0
- if there is a tie for highest, returns None (skip training)
"""
m = max(t6, t7, t8)
winners = (t6 == m) + (t7 == m) + (t8 == m)
if winners != 1:
return None
if t6 == m:
return 180.0
elif t7 == m:
return 90.0
else:
return 0.0
def color_tmp_triplet(t6: float, t7: float, t8: float) -> str:
ref = teacher_target_deg(t6, t7, t8) # 180/90/0 or None
vals = [t6, t7, t8]
if ref is None:
return ", ".join(f"{C_TMP_OTHER}{v:5.2f}{C_RESET}" for v in vals)
if ref == 180.0:
win = 0
elif ref == 90.0:
win = 1
else:
win = 2
out = []
for i, v in enumerate(vals):
c = C_TMP_WIN if i == win else C_TMP_OTHER
out.append(f"{c}{v:5.2f}{C_RESET}")
return ", ".join(out)
# =============================================================================
# Output normalization
# =============================================================================
def norm_y(y_deg: float) -> float:
"""0..180 -> -1..+1"""
return (y_deg - 90.0) / 90.0
def denorm_y(y_unit: float) -> float:
"""-1..+1 -> 0..180"""
return 90.0 + 90.0 * y_unit
# =============================================================================
# Feature engineering: temperature differences
# =============================================================================
def make_features(t6: float, t7: float, t8: float) -> List[float]:
mn = min(t6, t7, t8)
return [(t6 - mn) / DIFF_SCALE, (t7 - mn) / DIFF_SCALE, (t8 - mn) / DIFF_SCALE]
# =============================================================================
# Tiny MLP Regression: 3 -> H -> 1 (linear output)
# =============================================================================
class TinyRegMLP:
def __init__(self, hidden: int, lr: float, seed: int) -> None:
rnd = random.Random(seed)
self.h = hidden
self.lr = lr
# Layer 1: 3 -> H
self.W1 = [[(rnd.random() * 2 - 1) * 0.25 for _ in range(3)] for _ in range(hidden)]
self.b1 = [(rnd.random() * 2 - 1) * 0.05 for _ in range(hidden)]
# Layer 2: H -> 1
self.W2 = [(rnd.random() * 2 - 1) * 0.25 for _ in range(hidden)]
self.b2 = 0.0 # output in unit space (-1..+1-ish)
self.grad_clip = 5.0
def forward_unit(self, t6: float, t7: float, t8: float) -> Tuple[float, List[float], List[float], List[float]]:
x = make_features(t6, t7, t8)
pre = [0.0] * self.h
h = [0.0] * self.h
for i in range(self.h):
s = self.b1[i]
s += self.W1[i][0] * x[0]
s += self.W1[i][1] * x[1]
s += self.W1[i][2] * x[2]
pre[i] = s
h[i] = lrelu(s)
y = self.b2
for i in range(self.h):
y += self.W2[i] * h[i]
return y, x, pre, h
def predict_deg(self, t6: float, t7: float, t8: float) -> float:
y_unit, *_ = self.forward_unit(t6, t7, t8)
return denorm_y(y_unit)
def train_step(self, t6: float, t7: float, t8: float, target_deg: float) -> float:
target_unit = norm_y(target_deg)
y_unit, x, pre, h = self.forward_unit(t6, t7, t8)
err = (y_unit - target_unit)
loss = err * err
dL_dy = 2.0 * err
if dL_dy > self.grad_clip:
dL_dy = self.grad_clip
elif dL_dy < -self.grad_clip:
dL_dy = -self.grad_clip
W2_old = self.W2[:]
# Update output layer
for i in range(self.h):
self.W2[i] -= self.lr * (dL_dy * h[i])
self.b2 -= self.lr * dL_dy
# Backprop into hidden layer
for i in range(self.h):
dL_dh = dL_dy * W2_old[i]
dL_dpre = dL_dh * dlrelu(pre[i])
self.W1[i][0] -= self.lr * (dL_dpre * x[0])
self.W1[i][1] -= self.lr * (dL_dpre * x[1])
self.W1[i][2] -= self.lr * (dL_dpre * x[2])
self.b1[i] -= self.lr * dL_dpre
return loss
# =============================================================================
# Main
# =============================================================================
def print_settings() -> None:
print("\n" + "=" * 78)
print("BIR NN DEMO (REGRESSION) | TMP_6, TMP_7, TMP_8 -> SRV_4")
print("-" * 78)
print("Teacher rule (targets):")
print(" max(T6,T7,T8) == T6 -> 180°")
print(" max(T6,T7,T8) == T7 -> 90°")
print(" max(T6,T7,T8) == T8 -> 0°")
print(" tie for highest -> None (skip training sample)")
print("-" * 78)
print("Key idea:")
print(" NN input uses temps relative to MIN (offset-invariant).")
print(f" features = [(T6-min)/{DIFF_SCALE}, (T7-min)/{DIFF_SCALE}, (T8-min)/{DIFF_SCALE}]")
print("-" * 78)
print("BIR:")
print(f" IP={BIR_IP} TMP_IDS={TMP_IDS} SERVO={SERVO_ID} period={PERIOD_S}s timeout={TIMEOUT_S}s")
print("Training:")
print(
f" synthetic temps = mix: {TRAIN_RATIO*100:.0f}% micro({TRAIN_Tmicro_MIN:.1f}..{TRAIN_Tmicro_MAX:.1f}) + "
f"{(1-TRAIN_RATIO)*100:.0f}% normal({TRAIN_T_MIN:.1f}..{TRAIN_T_MAX:.1f}) °C"
)
print(f" epochs={EPOCHS} steps/epoch={STEPS_PER_EPOCH} hidden={HIDDEN} lr={LR} seed={SEED}")
print("=" * 78 + "\n")
def main() -> None:
rnd = random.Random(SEED)
model = TinyRegMLP(hidden=HIDDEN, lr=LR, seed=SEED)
print_settings()
# ---- Training ----
print("Training on synthetic data...\n")
for ep in range(1, EPOCHS + 1):
loss_sum = 0.0
trained = 0
last_y = 90.0
for step_in_ep in range(1, STEPS_PER_EPOCH + 1):
# Mixture sampling: micro range (near ties) vs normal wide range
if rnd.random() < TRAIN_RATIO:
lo, hi = TRAIN_Tmicro_MIN, TRAIN_Tmicro_MAX
else:
lo, hi = TRAIN_T_MIN, TRAIN_T_MAX
# Quantized temperatures (sensor-like)
t6 = quantize_temp(rnd.uniform(lo, hi))
t7 = quantize_temp(rnd.uniform(lo, hi))
t8 = quantize_temp(rnd.uniform(lo, hi))
# Teacher (skip tie)
y = teacher_target_deg(t6, t7, t8)
if y is None:
# still show progress sometimes, but do not train on this sample
if (step_in_ep % PROGRESS_EVERY) == 0 or step_in_ep == STEPS_PER_EPOCH:
mse = (loss_sum / trained) if trained > 0 else 0.0
nn = model.predict_deg(t6, t7, t8)
err = nn - last_y
live_line(
f"ep {ep:02d}/{EPOCHS} step {step_in_ep:6d}/{STEPS_PER_EPOCH} "
f"trained={trained:6d} mse={mse:.6f} "
f"T={t6:5.2f}, {t7:5.2f}, {t8:5.2f} "
f"REF={C_GREEN}{last_y:5.0f}{C_RESET} "
f"NN={C_ORANGE}{nn:6.1f}{C_RESET} "
f"err={err:6.1f} {C_TMP_OTHER}(tie-skip){C_RESET}"
)
continue
# Train on valid sample
last_y = y
loss = model.train_step(t6, t7, t8, y)
loss_sum += loss
trained += 1
# Live progress line
if (step_in_ep % PROGRESS_EVERY) == 0 or step_in_ep == STEPS_PER_EPOCH:
mse = (loss_sum / trained) if trained > 0 else 0.0
nn = model.predict_deg(t6, t7, t8)
err = nn - last_y
live_line(
f"ep {ep:02d}/{EPOCHS} step {step_in_ep:6d}/{STEPS_PER_EPOCH} "
f"trained={trained:6d} mse={mse:.6f} "
f"T={t6:5.2f}, {t7:5.2f}, {t8:5.2f} "
f"REF={C_GREEN}{last_y:5.0f}{C_RESET} "
f"NN={C_ORANGE}{nn:6.1f}{C_RESET} "
f"err={err:6.1f}"
)
sys.stdout.write("\n")
print("\nTraining done.")
print("Now running on REAL BIR: SRV_4 = NN output (continuous). Ctrl+C to stop.\n")
# Initialize servo to neutral position
try:
bir_set_value(BASE, SERVO_ID, 90.0)
except Exception as e:
print(f"[WARN] init servo failed: {e}")
step = 0
next_tick = time.monotonic()
# ---- Real-time loop ----
while True:
next_tick += PERIOD_S
step += 1
try:
t6, t7, t8 = bir_peer_multi(BASE, TMP_IDS)
ref = teacher_target_deg(t6, t7, t8) # 0/90/180 or None
nn = model.predict_deg(t6, t7, t8) # continuous (may overshoot)
ref_str = " tie " if ref is None else f"{ref:6.1f}"
err_str = " tie" if ref is None else f"{(nn - ref):7.1f}"
# Servo command is clamped and rounded to integer degrees
srv = int(clamp(round(nn), 0, 180))
bir_set_value(BASE, SERVO_ID, float(srv))
print(
f"[{step:05d}] TMP= {color_tmp_triplet(t6, t7, t8)} | "
f"REF={C_GREEN}{ref_str}{C_RESET} "
f"NN={C_ORANGE}{nn:6.1f}{C_RESET} err={err_str} | "
f"SRV_4={srv:3d}"
)
except (requests.RequestException, BirApiError, ValueError) as e:
print(f"[ERROR] {e}")
# Keep constant period
dt = next_tick - time.monotonic()
if dt > 0:
time.sleep(dt)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nStopped.")
If you just want to quickly test the BIR HTTP API for reading and writing values in a fixed-period loop, here is a minimal example that does not use any ML, but still keeps a persistent session and handles errors gracefully. The script continuously reads a set of sensor values, generates random values for a set of outputs, and writes them back to the BIR module. The console output shows the read values, the written values, and the time taken for each exchange.
Value exchange is limited to 10 inputs and 10 outputs. Testing showed that on a typical Wi-Fi connection, exchanging ten inputs and ten outputs can reach a transfer time of around 50 ms. The demo also uses the integrated BASIC compute layer to generate random values for VAL_40 through VAL_44. This makes it possible to combine both programmable layers.
# =============================================================================
# BIR FAST EXCHANGE DEMO (READ + WRITE in one call, fixed-period loop)
#
# Goal:
# - Quickly test the BIR HTTP API "exchange" endpoint in a tight loop.
# - One HTTP request does:
# * READ : a fixed list of sensor IDs
# * WRITE : a fixed list of output IDs (random values)
#
# Endpoint:
# POST {BASE}/exchange?read=ID1,ID2,ID3,...
# Body: "OUT_ID,val;OUT_ID,val;..."
#
# What you see in console:
# - One-line status with:
# * exchange time (ms)
# * READ values
# * WRITE values
# - Errors are printed as ERROR: ...
#
# Notes:
# - Uses requests.Session() to keep TCP connection alive (faster, lower overhead).
# - 503 is treated as NOT_READY (device busy / not ready).
# - Fixed-period scheduling tries to keep PERIOD_S constant (resync on overrun).
# =============================================================================
import time
import random
from typing import List, Tuple
import requests
# =========================
# USER MACROS / SETTINGS
# =========================
BASE = "http://192.168.0.202" # <-- Set your BIR / HUB base URL (IP or hostname)
TIMEOUT_S = 2.0
PERIOD_S = 0.1 # Fixed loop period (seconds). 0.1 = 100 ms
READ_IDS: List[str] = ["TMP_6", "TMP_7", "TMP_8", "INP_3", "DAY_SYS", "VAL_40", "VAL_41", "VAL_42", "VAL_43", "VAL_44"]
WRITE_IDS: List[str] = ["SRV_4", "SRV_5", "OUT_0", "OUT_1", "OUT_2", "VAL_35", "VAL_36", "VAL_37", "VAL_38", "VAL_39"]
WRITE_RANGES = {
"SRV_4": (0.0, 180.0, 1),
"SRV_5": (0.0, 180.0, 1),
"OUT_0": (0.0, 1.0, 0),
"OUT_1": (0.0, 1.0, 0),
"OUT_2": (0.0, 1.0, 0),
"VAL_35": (-100000.0, 100000.0, 2),
"VAL_36": (-100000.0, 100000.0, 2),
"VAL_37": (-100000.0, 100000.0, 2),
"VAL_38": (-100000.0, 100000.0, 2),
"VAL_39": (-100000.0, 100000.0, 2),
}
DEFAULT_RANGE = (0.0, 1.0, 2)
# =========================
# BIR EXCHANGE (keep-alive)
# =========================
class BirApiError(RuntimeError):
pass
sess = requests.Session()
def bir_exchange(
node_base: str,
read_ids: List[str],
write_pairs: List[Tuple[str, float]],
timeout_s: float = TIMEOUT_S,
) -> List[float]:
params = {"read": ",".join(read_ids)}
body = ";".join(f"{sid},{val}" for sid, val in write_pairs)
r = sess.post(f"{node_base}/exchange", params=params, data=body, timeout=timeout_s)
if r.status_code == 503:
raise BirApiError("NOT_READY (503)")
if r.status_code in (400, 404, 413):
raise BirApiError(f"{r.status_code} {r.text.strip()}")
r.raise_for_status()
txt = r.text.strip()
parts = [p.strip() for p in txt.split(";")] if txt else []
if len(parts) != len(read_ids):
raise BirApiError(f"Parse mismatch: got {len(parts)} values, expected {len(read_ids)}. Raw='{txt}'")
return [float(p.replace(",", ".")) for p in parts]
def gen_random_value(sensor_id: str) -> float:
mn, mx, dec = WRITE_RANGES.get(sensor_id, DEFAULT_RANGE)
v = random.uniform(mn, mx)
if dec <= 0:
return float(int(round(v)))
return round(v, dec)
def main():
print("BIR exchange demo (fixed period loop, keep-alive session)")
print("BASE:", BASE)
print("READ:", READ_IDS)
print("WRITE:", WRITE_IDS)
print("PERIOD_S:", PERIOD_S)
print("----")
last_len = 0
try:
next_t = time.perf_counter() # scheduled time of the next iteration
try:
while True:
loop_start = time.perf_counter()
write_pairs = [(sid, gen_random_value(sid)) for sid in WRITE_IDS]
try:
read_vals = bir_exchange(BASE, READ_IDS, write_pairs)
ok = True
err_txt = ""
except BirApiError as e:
ok = False
err_txt = str(e)
read_vals = []
loop_end = time.perf_counter()
dt_ms = (loop_end - loop_start) * 1000.0
# Prepare message
if ok:
read_str = ", ".join(f"{sid}={val}" for sid, val in zip(READ_IDS, read_vals))
write_str = ", ".join(f"{sid}={val}" for sid, val in write_pairs)
msg = f"[one exchange{dt_ms:7.1f} ms] READ: {read_str} | WRITE: {write_str}"
else:
msg = f"[one exchange{dt_ms:7.1f} ms] ERROR: {err_txt}"
# Single-line print
pad = " " * max(0, last_len - len(msg))
print("\r" + msg + pad, end="", flush=True)
last_len = len(msg)
# Fixed-period scheduling (sleep only the remaining time)
next_t += PERIOD_S
sleep_s = next_t - time.perf_counter()
if sleep_s > 0:
time.sleep(sleep_s)
else:
# Overrun: we're late. Resync to "now" to avoid drift.
next_t = time.perf_counter()
except KeyboardInterrupt:
print("\nStopped by user (Ctrl+C).")
finally:
sess.close()
if __name__ == "__main__":
main()
The RLY (relay) module is designed for safe, galvanically isolated switching of electrical loads. It uses LEG-5 relays. The module provides four independent channels. The MCU that communicates with the BIR module over the bus is powered from 3.3 V, while the relay coils are powered from 5 V.
The INP module is built around LTV-847S optocouplers. The board uses two 4-channel devices, providing 8 galvanically isolated input channels split into two independent 4-channel banks. Each bank has its own dedicated ground reference (GND) pin; using a jumper, this ground can be either tied to the common system GND or left isolated for a fully independent input group. Input speed is determined by the LTV-847S: typical switching/transient times are on the order of a few microseconds (rise time typ. 4 µs, fall time typ. 3 µs; often also specified as turn-on/turn-off typ. ~4 µs). The maximum input voltage, as across the entire Bugitrun platform, is limited to 30 V.
The ODP (open-drain PWM) module is designed for controlling outputs using open-drain switching with PWM support. It provides six channels, arranged into three frequency pairs (1–2, 3–4, 5–6). Within each pair, both channels share the same PWM frequency, while the duty cycle is controlled independently per channel. The frequency can be set from 500 Hz to 50 kHz, and the duty cycle from 0 to 100%. The frequency is always set by the first channel in each pair (CH1/CH3/CH5) by briefly applying a value in the range 500 to 50,000; afterwards, the channels are controlled only by duty-cycle values. The maximum external voltage, as across the entire Bugitrun platform, is limited to 30 V. The maximum output power per channel is listed in the table printed directly on the PCB. High-power PWM switching is handled by T_N_AO4268 MOSFETs, driven with sufficiently fast edges by the SN74ACT14, providing a solid performance-to-cost compromise.
ANL (analog voltage input) module is an 8-channel ADC expansion module designed for measuring external voltages using precision resistive dividers. All channels measure 0 to 30 V relative to GND and are sampled by a 12-bit ADC (4092 steps), providing an effective resolution of approximately 7 mV across the full range. The module is not galvanically isolated; all inputs share the system ground. For robustness, each channel includes input protection with clamping (Schottky) diodes to reduce the risk of damage during brief over-voltage events. As across the entire Bugitrun platform, the maximum external voltage is limited to 30 V.
SRV (servo) module is designed as a universal 8-channel servo signal expander for standard RC servos. Each channel outputs a precise 50 Hz control signal (20 ms frame) with configurable pulse width limits (typically 1000–2000 µs), while the module accepts position commands as float values with multiple motion profiles. Mode 0 provides direct positioning (0–180°). Mode 1 performs ultra-slow stepped motion (1000–1180 → 0–180°) using 1° increments with very gentle pacing, suitable for extremely smooth or mechanically sensitive movement. Mode 2 performs slow stepped motion (2000–2180 → 0–180°) using 1° increments with faster pacing for smoother yet more responsive transitions. Mode 3 performs a smooth acceleration/deceleration ramp (3000–3180 → 0–180°) using a non-blocking trajectory engine. Servo power is supplied via a dedicated external PWR input and is distributed to all servo connectors; users must provide the correct voltage for their specific servos. Logic power for the controller is taken from the Bugitrun bus, and grounds are shared to ensure proper signal reference. As across the entire Bugitrun platform, the external supply voltage must not exceed 30 V.
Try the Cloud Dashboard demo — password: “bugabuga”.
Demo Dashboard