Bugitrun Logo

HOME

Bugitrun

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.

Ecosystem Modules
Open module ecosystem on a simple plug-and-play bus. New channels appear automatically in the dashboards.
Runs Offline
Your BASIC program runs inside the BIR module — even when the dashboard is closed or the internet goes down.
BIR Network
Link multiple BIR modules into one local network for shared monitoring and automation.
Neural Network Support
Use external Python scripts to train and run neural networks on BIR devices.

Quick Start

  1. Power the BIR module (USB-C).
  2. Connect to Wi-Fi BUGITRUN_SETUP.
  3. Open http://192.168.4.1 and enter your Wi-Fi + Auth Key + time zone.
  4. Click the link to the Cloud dashboard for remote access, or switch to the fast Local dashboard.
  5. Plug an external module → new tiles appear automatically.
  6. Toggle values directly, or write a small BASIC program and upload it.

About the project

WHY BUGITRUN WAS CREATED

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.


WHAT IS BUGITRUN

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.


HOW BUGITRUN WORKS

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.

Manual

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.

FIRST USE

If the BIR module starts without stored configuration, it automatically enters initialization mode:

  • a temporary Wi-Fi network BUGITRUN_SETUP is created,
  • the password for this network is adminadmin.

The initialization procedure is as follows:

  1. Connect a phone, tablet or computer to the Wi-Fi network BUGITRUN_SETUP (password adminadmin).
  2. Open a web browser and go to http://192.168.4.1.
  3. In the configuration form, fill in:
    • SSID – the name of the target Wi-Fi network,
    • Password – the password of this Wi-Fi network,
    • Auth Key – access key for the cloud dashboard (minimum length 8 characters),
    • Time Zone – select from the list or enter a custom POSIX string.
  4. Submit the form using the Save button.

The module stores the provided data, connects to the configured Wi-Fi network and attempts to complete the initialization with the cloud backend.

STATUS INDICATION (LED)

After the initialization attempt, the result is indicated using the status LED:

  • LED permanently on – initialization completed successfully.
  • LED blinking – initialization failed (Wi-Fi / connectivity / server error).

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.

REPEATED INITIALIZATION

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.

COMMON FEATURES OF BOTH DASHBOARDS

Both the Local and Cloud Dashboards share a common set of core features to ensure a consistent experience.

RENAMING THE BIR MODULE

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).

TILE DISPLAY

Both dashboards display sensors and outputs as tiles, each identified by:

  • a three-letter ID,
  • a user-defined name,
  • a float value.

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_0OUT_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).

RENAMING OBJECTS

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.

EDITING OUTPUTS

For outputs (e.g. relays), you can click on the value and enter a new float value, which is then sent to the device.

BASIC EDITOR

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.

IMPORT / EXPORT

Both dashboards allow you to:

  • export a project to a file,
  • import a project from a file.

This makes it easy to move projects between the Local and Cloud Dashboards or between different BIR modules.

ERASE BUTTON

Each dashboard includes an Erase button with two functions:

  • clear local storage data in your browser,
  • erase the BASIC program from the BIR module and restart the device.

CLOUD DASHBOARD

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.

OBJECT GRID

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.

BASIC EDITOR AND PROJECT MANAGEMENT

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:

  • load a project,
  • save a project,
  • rename a project,
  • delete a project,
  • run a static code check,
  • upload the program to the BIR module.

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.

CORNER MENU

The upper-right corner menu provides these actions:

  • switch to the Local Dashboard,
  • show or hide the graph panel,
  • export a project,
  • import a project,
  • reset the dashboard,
  • update firmware,
  • change the visual skin.

SWITCH TO LOCAL DASHBOARD

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.

GRAPH PANEL

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:

  • assign up to 10 sensors to 10 graph slots,
  • choose left or right Y-axis per sensor,
  • adjust the visible time range,
  • set minimum and maximum value limits,
  • pause drawing,
  • reset the graph,
  • export collected data as a CSV file.

EXPORT AND IMPORT

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.

RESET DASHBOARD

Clears the dashboard state and prepares it for fresh synchronization with the device.

FIRMWARE UPDATE

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.

SKIN SELECTION

Cycles through the available visual skins. Skins rotate in sequence, and additional designs may be added over time.

ONE-SESSION LOCK

The Cloud Dashboard includes a global one-session lock.

  • Only one active Cloud Dashboard session is allowed across the entire internet for a given device.
  • If a second browser or device tries to open the Cloud Dashboard simultaneously, it is blocked to prevent conflicting remote operations.
  • A visible notice appears, and the user may take over the session via a dedicated button.
  • When the takeover is confirmed, the previous session is safely terminated and control is transferred to the new browser.

This mechanism prevents race conditions and ensures that commands sent from the cloud remain consistent and secure.

TYPICAL USAGE

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.


LOCAL DASHBOARD

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:

  • responses are instant,
  • the interface remains fully functional even without an internet connection.

OBJECT GRID

The main area displays a live grid of tiles:

  • Each tile represents a sensor, output, or time-related object.
  • Values update immediately as the device processes new data.

BASIC EDITOR

Below the grid, the Local Dashboard provides a BASIC code editor for writing and modifying programs. The editor includes:

  • Static code check – same as on the Cloud Dashboard: validates your program before upload.
  • Upload CODE – same as on the Cloud Dashboard: sends the prepared program to the device in its compact internal format for execution.
  • Automatic browser storage – your work is continually saved locally in the browser, preventing data loss on reload.

In addition, the Local Dashboard supports system-level commands directly inside the BASIC editor:

  • Syntax: SYSTEM(COMMAND)
  • Behavior: When the program is uploaded, any SYSTEM() instruction is executed immediately and directly on the BIR module.
  • Scope: This capability is local-only for safety reasons and enables powerful maintenance actions, such as:
    • emergency firmware recovery,
    • resetting internal components,
    • forcing a critical system update.

Command description:

  • SYSTEM(ENABLE_INSECURE_OTA_ONCE) – enables a one-time emergency OTA update without certificate validation.
    • Recommended when secure TLS/certificate validation is broken, for example after a Cloudflare certificate change.
    • Updates the BIR module core firmware only. It does not update the Local Dashboard. To update both the core and the Local Dashboard, perform a standard OTA update from the Cloud Dashboard.

Other SYSTEM() commands may be used in the same way.

QUICK-ACCESS MENU

A small floating menu provides essential local tools:

  • Export project – same as on the Cloud Dashboard: saves the current project (BASIC code + object names) as a JSON file.
  • Import project – same as on the Cloud Dashboard: loads a previously exported JSON project file.
  • Reset Dashboard & Restart – similar to Reset Dashboard on the Cloud Dashboard, but can additionally erase the BASIC program on the device and reboot the module.

ONE-SESSION LOCK

The Local Dashboard enforces a single active session only within the same browser environment.

  • The lock applies between tabs of the same browser.
  • Technically, the module can still be accessed from:
    • another device on the LAN,
    • another browser on the same device,
    • a different platform entirely.

Although this is allowed, it is strongly discouraged to run the Local Dashboard on multiple devices or browsers simultaneously, because it can:

  • interfere with the BIR module’s local communication timing,
  • cause rapid conflicting commands,
  • overload internal channels,
  • break the expected network flow.

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.

TYPICAL USAGE

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.

Power

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.

Built-in outputs and inputs

  • OUT_0, OUT_1, OUT_2 – three built-in 5 V outputs.
    Each output is protected by a diode and can drive up to approximately 1 A. Inductive loads (relays, small coils, etc.) can be connected directly within this limit.
  • INP_3 – one built-in digital input.
    The input is galvanically isolated via an optocoupler and accepts voltages in the range 0–30 V. On the dashboards and in BASIC it is represented as a numeric value (typically 0 or 1).
  • SRV_4, SRV_5 – two built-in servo outputs.
    They are designed for standard 5 V RC servos. The expected value range is 0–180 (degrees); values outside this range are internally clamped by the firmware.

Temperature sensors (DS18B20)

  • Connected DS18B20 sensors are mapped to IDs TMP_6TMP_15 (up to 10 sensors in total).
  • If more than one DS18B20 is present, they are automatically ordered by their internal serial number. This means that after each restart the same physical sensor will appear under the same TMP_x ID.
  • If no DS18B20 sensors are connected, no TMP_x objects are created.

System time and date objects

  • 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.

Generic value slots – VAL_x

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.

Base platform characteristics

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.

Communication with dashboards

  • Local Dashboard – communicates with the BIR module over plain HTTP (no encryption). Intended for local network use.
  • Cloud Dashboard – communicates over HTTPS using ECDSA and HMAC for authentication and integrity protection.

Control LED

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.

SINGLE-VALUE ACCESS – GET

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:

  • IP – IP address of the remote BIR (string),
  • ID – ID of the remote object (e.g. OUT_0, TMP_1, VAL_10),
  • timeout_ms – optional timeout in milliseconds.

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:

  • BIRNET_SINGLE_SERVER_demo (B) – toggles OUT_0 on the server module.
  • BIRNET_SINGLE_CLIENT_demo (A) – reads OUT_0 from the server and mirrors the LED.

MULTI-VALUE SYNC – BIRNETSYNC

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:

  1. BIRNETSET(IP, timeout) – define which remote BIR to talk to.
  2. SERVERIDS(...) – list of remote IDs to read from the server.
  3. CLIENTIDS(...) – list of local IDs to overwrite on the client.
  4. BIRNETSYNC – perform the actual multi-value synchronization.

BIRNETSET(IP, timeout)

Configures the target BIR and timeout for all subsequent BIRNETSYNC calls:

BIRNETSET("192.168.0.200", 300)
  • IP – IP address of the remote BIR.
  • timeout – timeout in milliseconds, internally clamped to 300–5000 ms.

SERVERIDS(...)

Defines which remote IDs will be read from the server. Up to 10 IDs are supported:

SERVERIDS(OUT_0, OUT_1, OUT_2)

CLIENTIDS(...)

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.

BIRNETSYNC

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:

  • If the HTTP request succeeds and the server returns the correct number of values, all listed CLIENTIDS are updated in one step.
  • If any error occurs (timeout, server not ready, HTTP error, or an unknown ID), none of the local values are changed – the previous state remains active.

This makes multi-sync safe for professional automation scenarios where partial updates could cause inconsistent states or unwanted behavior.

BIRNET MULTI demo:

  • BIRNET_MULTI_SERVER_demo – generates three random boolean outputs on the server.
  • BIRNET_MULTI_CLIENT_demo – uses BIRNETSET / SERVERIDS / CLIENTIDS / BIRNETSYNC to mirror these three outputs on the client.

BIRNETSTAT

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)
            

LIMITATIONS AND BEST PRACTICES

  • BIRNET communication is intended for trusted local networks (LAN). It does not replace the secure HTTPS cloud channel.
  • Both GET() and BIRNETSYNC are blocking operations: they pause the BASIC interpreter for the configured timeout on each call.
  • Always verify IDs in 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.

DOWNLOADS:

LET – Assign a numeric value

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.

Syntax

LET NAME = numeric_expression

Rules

  • NAME can be:
    • an existing object:
      • a system ID (e.g. 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), or
      • a user-defined name assigned to that object in the dashboard (e.g. heater, room_temp).
      System IDs and their user names always refer to the same underlying object.
    • or a standalone hidden numeric variable whose name matches the pattern:
      three uppercase letters, an underscore, and digits, e.g. AVG_1, DEL_123, MIX_42.
      If such an ID does not correspond to any existing tile, the interpreter creates a hidden variable with that name. These hidden variables exist only in program memory and are not shown as tiles on the dashboard.
  • NAME must not be a reserved keyword, function or constant (e.g. IF, THEN, TIME, DATE, DAY, ABS, RND, PI, BIRNETSTAT, …).
  • The right side must be a pure numeric expression.
  • Boolean operators (AND, OR, NOT) are not allowed inside numeric expressions. They are reserved for boolean expressions in IF conditions.
  • Expressions may include:
    • numbers (float),
    • system IDs (e.g. TMP_6, OUT_0, VAL_20, TIM_SYS) or their user-defined names (e.g. heater, room_temp),
    • hidden variables matching the AAA_123 pattern (e.g. AVG_1, MIX_1, DEL_10),
    • arithmetic operators +, -, *, /,
    • unary + and -,
    • parentheses ( ... ),
    • built-in functions such as ABS, MAX, MIN, SIN, COS, TAN, RND, LOG, LN, EXP, ROUND, FLOOR, CEIL, SQRT, …,
    • constants 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(...).

Examples

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

IF … THEN … ELSE – Conditional branching

The IF instruction evaluates a boolean expression and, depending on the result, executes one of two possible actions:

  • the THEN branch when the condition is true,
  • the optional ELSE branch when the condition is false.

Syntax

IF boolean_expression THEN single_command
IF boolean_expression THEN single_command ELSE single_command
  • boolean_expression – condition that is evaluated to true / false.
  • single_command – exactly one BASIC command on the same line.

After THEN and ELSE you can use:

  • LET ...
  • DELAY ...
  • GOTO ...
  • GOSUB ...
  • RETURN
  • PRINT ...
  • or another nested 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

Boolean expressions in IF support:

  • numeric expressions (same syntax as in LET),
  • comparison operators,
  • logical operators,
  • parentheses.

Comparison operators:

==
=
!=
<
>
<=
>=

Each side of the comparison is a numeric expression, for example:

  • TMP_6 > 21
  • (TMP_6 + TMP_7) / 2 <= 25
  • ABS(room_temp - 21) < 0.5
  • BIRNETSTAT = 200

Logical operators:

  • AND
  • OR
  • NOT

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,
  • then AND,
  • then 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:

  • true if the result is not equal to 0,
  • false if the result is exactly 0.

THEN / ELSE actions

  • If the condition is true, the command after THEN is executed.
  • If the condition is false and an ELSE part is present, the command after ELSE is executed.
  • If the condition is false and there is no 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.

Examples

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

GOTO – Unconditional jump

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.

Syntax

GOTO lineNumber

Rules

  • lineNumber must match the number at the beginning of an existing BASIC line (for example 10, 100, 250).
  • The dashboard check verifies that all 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:
    • as a standalone command (e.g. 20 GOTO 100),
    • after THEN or ELSE in an IF line (e.g. 10 IF VALUE > 0 THEN GOTO 100).
  • Only one command is allowed on each line – you cannot write multiple commands separated by : or similar.

Example program (IF AS CASE DEMO)

# ===============
# 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 / RETURN – Subroutines

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.

Syntax

GOSUB lineNumber
RETURN

Rules for GOSUB

  • lineNumber must be an existing line in the program. The dashboard check validates all GOSUB targets before upload and reports calls to missing lines as errors.
  • When GOSUB is executed:
    • the current line position is pushed onto an internal call stack,
    • execution jumps to the target line (the subroutine start).
  • Subroutines can call other subroutines (nested GOSUB calls).
  • The maximum nesting depth is 20 active GOSUB levels. If this limit is reached, further 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.

Rules for RETURN

  • RETURN should be placed at the end of a subroutine.
  • When RETURN is executed:
    • the last saved position from GOSUB is taken from the stack,
    • execution continues with the line after that GOSUB.
  • If RETURN is executed when no GOSUB is active, it has no effect and execution simply continues with the next line.
  • If execution falls out of a subroutine without reaching RETURN, it continues into the following lines. For clarity, it is recommended to always end subroutines with an explicit RETURN.

Basic subroutine example

10 GOSUB 100
20 DELAY 1000
30 GOTO 10

100 LET LED = 1
110 DELAY 200
120 LET LED = 0
130 RETURN

GOSUB used with IF

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.

Example program (GOSUB DEMO)

# =========
# 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

FOR / TO / STEP / NEXT – Counting loops

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.

Syntax

FOR var = start TO end
FOR var = start TO end STEP step_value
...
NEXT
NEXT var

Rules

  • var is a numeric NAME:
    • a system ID (e.g. VAL_10, OUT_0, TMP_6),
    • or a user-defined object name (e.g. LOOP, SPEED, INC1),
    • or a hidden variable matching the AAA_123 pattern (e.g. LOP_1).
    The loop variable is stored like any other numeric object – you can read it in expressions inside the loop.
  • start, end and step_value are numeric expressions (floats are supported).
  • STEP is optional:
    • if omitted, STEP 1 is used by default,
    • a negative STEP is allowed (counting down),
    • a very small or zero STEP is not recommended, because it makes the loop effectively run only once.
  • Each 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.
  • Inside the loop you can use any valid BASIC commands (LET, IF, DELAY, GOSUB, PRINT, etc.).

Simple examples

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

Example program (FOR NEXT STEP DEMO)

# ==================
# 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

DELAY – Pause execution

The DELAY instruction pauses program execution for a specified number of milliseconds.

Syntax

DELAY ms_expression

Rules

  • ms_expression is a numeric expression evaluated to a delay in milliseconds (floats are allowed and are rounded to the nearest millisecond).
  • If the result is zero or negative, the delay is skipped and execution continues with the next line.
  • Internally, 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).
  • After a WAITCLOUDTICK, the next DELAY automatically re-synchronizes its internal time anchor to the current moment, so timing continues smoothly from the cloud tick.
  • During 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.

Example

10 LET MS = 200
20 LET LED = 1
30 DELAY MS
40 LET LED = 0
50 DELAY MS
60 GOTO 20

WAITINIT – Wait for initialization

The WAITINIT instruction waits until the module finishes initialization and all connected sensors (DS18B20 and external modules) have their first valid readings.

Syntax

WAITINIT

Rules

  • WAITINIT takes no arguments.
  • It is usually placed at the beginning of the program (often on line 0), but it can be used on any line. If initialization is already finished, WAITINIT returns immediately.
  • While initialization is still in progress, the BASIC interpreter repeatedly stays on the WAITINIT line and sleeps for short intervals, so the rest of the program does not run yet.
  • Without 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.

Example (WAITINIT demo)

# =============
# 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

WAITCLOUDTICK – Wait for the next cloud tick

The WAITCLOUDTICK instruction blocks the BASIC program until the cloud server signals a new synchronization tick (when the online dashboard is active).

Syntax

WAITCLOUDTICK

Rules

  • WAITCLOUDTICK takes no arguments.
  • Execution stops on the line with WAITCLOUDTICK until the next cloud tick arrives. The BASIC interpreter repeatedly sleeps in short intervals while it is waiting.
  • When a new cloud tick arrives, WAITCLOUDTICK unblocks, the internal timing anchor for DELAY is re-synchronized, and execution continues on the next line.
  • If the online dashboard is closed and the module detects that the client is offline, the BASIC program remains parked on the 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.
  • A common pattern is to perform some action (e.g. blink an LED), then wait for the next cloud tick and optionally count successful ticks in a dedicated value.

Example (WAITCLOUDTICK demo)

# ====================
# 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

PRINT – Show a text label for a numeric value

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.

Syntax

PRINT "text", ID

Rules

  • text is a string literal enclosed in double quotes, up to 10 characters. Longer texts are automatically truncated to the first 10 characters.
  • ID must be an existing object ID exactly as shown on the tile (e.g. VAL_10, TMP_6, OUT_0, Door if it is the real ID, …).
  • If text is a non-empty string, that text is displayed instead of the raw numeric value on the corresponding tile.
  • If text is an empty string (""), the text mask is removed and only the numeric value is shown again.
  • The numeric value of the object itself always keeps its true floating-point value and can be used in all expressions and conditions, regardless of the active text mask.
  • PRINT can also be used after THEN or ELSE in an IF line as a single command.

Example (PRINT demo)

# ==========
# 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

TIME, DATE, DAY – System parameters

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.

  • TIME – current time of day as a floating-point number (hours with fractional part).
    For example, 17:30 is represented as 17.5, 6:15 as 6.25.
  • DATE – current date as day + month/100.
    For example, 9 January is 9.01, 31 December is 31.12.
  • DAY – current day of week as an integer: 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.
  • Their names are reserved keywords – you cannot rename objects to 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.
  • Using an exact equality such as TIME = 17:31 is not recommended, because the internal value and the rounded literal almost never match exactly.
  • Always use a time range in conditions, for example:
    IF TIME >= 17:31 AND TIME < 17:32 THEN ...

Example (DATETIME demo)

# =============
# 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

Math functions and constants

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 value
  • MIN(a, b), MAX(a, b) – minimum / maximum of two values
  • MOD(a, b) – remainder after division (floating-point modulus)
  • SIGN(x) – returns -1 for negative, 0 for zero, 1 for positive values
  • ROUND(x), FLOOR(x), CEIL(x) – rounding, floor and ceiling
  • SQRT(x) – square root
  • LOG(x) – base-10 logarithm
  • LN(x) – natural logarithm (base E)
  • EXP(x) – ex
  • SIN(x), COS(x), TAN(x) – trigonometric functions, x is in radians
  • RND() – random value in the range 0..1
  • RND(max) – random value in the range 0..max

Constants:

  • 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).

Example (Math DEMO)

# =========
# 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.

Neural Network example

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.

What the demo does

  • Train: Generate synthetic TMP_6/TMP_7/TMP_8 triplets and train a tiny NN (offline).
  • Teacher: Discrete targets 0° / 90° / 180° from a strict “winner” rule (ties skipped).
  • Read: Fetch live temperatures in one request via /peer_multi.
  • Predict: NN outputs a continuous servo angle.
  • Write: Send the angle to SRV_4 via /command.

Training phase (synthetic “teacher”)

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: , 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.

Live control loop (BIR as the actuator endpoint)

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.

WATCH THE VIDEO EXAMPLE


# =============================================================================
# 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.")
                  

Fast value exchange example

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.

WATCH THE VIDEO EXAMPLE


# =============================================================================
# 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()                  
                  

Ecosystem

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.

Videos

Live demo

Try the Cloud Dashboard demo — password: “bugabuga”.

Demo Dashboard

Contact

[email protected]
https://github.com/bugitrun
https://www.youtube.com/bugitrun