Mutiple files

Overview

Supporting multiple files in UbiFunctions enhances flexibility and scalability for developers. When creating serverless functions, the ability to structure code into multiple files improves maintainability, facilitates code reuse, and aligns with industry best practices for software development.

UbiFunctions allows you to use multiple files such as:

  • Source files: These files contain code that can be executed.

  • Config files: These are typically .json, .conf, .toml, or similar files that do not contain executable code but rather provide the data and parameters needed for your logic to run with a particular behaviour.

  • Data files: These are files that store information in various formats, such as .csv, .txt, or .json. They can contain structured, semi-structured, or unstructured data used as input for processing or analysis.

Creating a file

To create a file within your UbiFunction's root directory, click the + icon in the files pane and then select New file. Write a name. After doing so, a new file will be added to your UbiFunction:

Creating a folder

Folders help you organize and separate each module more effectively. To create a folder, click the + icon in the files pane and then select New folder. Write a name. After doing so, a new folder will be added to your UbiFunction:

Creating files or folders within a folder

To create a file or folder, hover over an existing folder, click the three-dot menu, and select New File or New Folder.

File Structure and importing

When you add files or folders to a UbiFunction, they behave like a directory on your local machine, with the root directory as the working directory. For example, adding decoder.py|js alongside the main script allows you to import it as:

const decoder = require('./decoder');

Similarly, if you create a folder utils and add a file parser.py or parser.js inside it, the structure behaves like a subdirectory on your computer. To import from it, you would use:

const parser = require('./utils/parser');

This means that when working in UbiFunctions, you can think of the file structure as if you are managing a local project folder.

UbiFunctions use a protected file system where you can create files and folders via the GUI, but scripts run in a strictly read-only environment where they cannot write to files or create new files and folders.

Data and config files

UbiFunctions allow you to include data and configuration files (e.g., .json, .conf, .txt, .csv) to provide inputs or settings for your functions. To read a file, you would handle it just as you would in a typical script on your local machine:

const fs = require('fs');

// Read a JSON file
// Adjust the file's path accordingly
const settings = JSON.parse(fs.readFileSync('settings.json', 'utf-8'));

Example

Suppose that you have a fleet of several different devices on an LNS constantly sending data. On each uplink, you require to decode that data and then forward it to the corresponding device in Ubidots.

For this, you can create an UbiFunction where you'll have separate scripts for each device's type decoder and properly use them from the main function to decode the data.

For the example, proceed to create:

  • A folder called modules. Within this folder, you'll create the following files:

    • A Python script called s2100.py. This is the decoder for the s2100 device.

    • A Python script called s210x.py. This is the decoder for the s210x device.

    • A Python script called utils.py. This one will contain all utils scripts.

  • A configuration file containing the configuration for each device type. This one is called device_config.json.

The following are the contents of each script:

s2100.py script

def s2100_decode(data: int) -> dict:
    """Decode data for s2100 device."""
    temperature = (data & 0x3) >> 0
    humidity = (data & (0x3 << 2)) >> 2
    battery = (data & (0x3 << 4)) >> 4

    return {
        "temperature": temperature,
        "humidity": humidity,
        "battery": battery
    }

s210x.py script

def s210x_decode(data: int) -> dict:
    """Decode data for s210x device."""
    soil_electrical_conductivity = (data & 0x3) >> 0
    battery = (data & (0x3 << 2)) >> 2

    return {
        "soil_electrical_conductivity": soil_electrical_conductivity,
        "battery": battery
    }

utils.py script

def parse_data(data_str: str) -> int:
    """Convert a binary string to an integer."""
    return int(data_str, 2)

device_config.json

{
    "s2100" : {"color" : "#fefefe"},
    "s210x" : {"color" : "#ffffff"}
}

main.py

import requests
import time
import json
import os
from modules.utils import parse_data
from modules.s2100 import s2100_decode
from modules.s210x import s210x_decode

decoders = {
    "s210x" : s2100_decode,
    "s2100" : s210x_decode
}

def main(args):

    """Decode payload based on device type."""
    device_type = args.get("device")
    data_str = args.get("data")
    
    if not device_type or device_type not in decoders:
        print(f"Device type: {device_type} not supported...")
        return {"status" : "error"}

    # Convert binary string to integer
    data = parse_data(data_str)
    decoded_data = decoders[device_type](data)

    # Open and read the JSON file
    with open("device_config.json", 'r') as file:
        config = json.load(file)

    print(config[device_type])
    print(decoded_data)
    return {"status" : "success" , "data" : decoded_data}

If you execute this UbiFunction with the following test payload:

{"device": "s2100", "data": "0b01011010"}

it should output something like this:

{'color': '#fefefe'}
{'soil_electrical_conductivity': 2, 'battery': 2}

Last updated