Bridge.provide() issues and method not available

Hi everyone, i'm trying to build a robot for a competition, using the Arduino UNO Q, the SPE SHIELD, and to it connected 4 Roller 485 motors from m5stack, i'm trying to expose from the Arduino side to the linux side, the functions that operate on the motors, however, no matter what approach i use, i can't seem to be able to provide and work reliably with functions through the Bridge. Can anybody link me to a more rich documentation? has anybody encountered my same issues? Next I'll show the code to reproduce the same issue on your machine, feel free to try it, or to give advice on anything I might be doing wrong...

I'm not providing the web panel html code as it is not relevant, the issue persists even if i do not use a web panel and just try to access those functions in the App.run user loop.

The issue:

  • when I try to access any provided function it says method not available
  • if i disable providing every function except for the ready function, that alone works
  • it doesn't matter wether i use Bridge.provide_safe() or just Bridge.provide()
  • I also tried making a single function that takes the command as argument and then arguments for operations if needed, and then sets those to global variables, then i try to take action in the sketch.ino loop() but that function either isnt available or gets stuck executing and i get nothing back.
  • I tried using Bridge.notify() even if it isnt suitable for String returning functions
  • I tried using Bridge.call().result and it still didn't work
  • I don't understand why providing a function to the bridge breaks all the provided functions, and none of them then work, "method not available".

The Code:

Python side (flask web panel to interact with the functions)

@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@app.route('/logs')
def get_logs():
    return jsonify(logs=logs)

@app.route('/clear_logs', methods=['POST'])
def clear_logs():
    global logs
    logs = []
    return jsonify(result="ok")

# --- API Endpoints mapped to Bridge ---

@app.route('/drive', methods=['POST'])
def drive():
    speed = int(request.form.get('speed', 0))
    steering = int(request.form.get('steering', 0))
    res = Bridge.call("differentialDrive", speed, steering)
    add_log(f"CMD: differentialDrive({speed}, {steering}) returned: {res}")
    return jsonify(result=res)

@app.route('/stop_all', methods=['POST'])
def stop_all():
    res = Bridge.call("stop_all")
    add_log(f"CMD: stop_all() returned: {res}")
    return jsonify(result=res)

@app.route('/motor_move', methods=['POST'])
def motor_move():
    mid = int(request.form.get('id', 1))
    speed = int(request.form.get('speed', 0))
    res = Bridge.call("move_single_motor", mid, speed)
    add_log(f"CMD: move_single_motor({mid}, {speed}) returned: {res}")
    return jsonify(result=res)

@app.route('/motor_stop', methods=['POST'])
def motor_stop():
    mid = int(request.form.get('id', 1))
    res = Bridge.call("stop_single", mid)
    add_log(f"CMD: stop_single({mid}) returned: {res}")
    return jsonify(result=res)

@app.route('/motor_enable', methods=['POST'])
def motor_enable():
    mid = int(request.form.get('id', 1))
    state = (request.form.get('state') == 'true')
    res = Bridge.call("set_enable_single", mid, state)
    add_log(f"CMD: set_enable_single({mid}, {state}) returned: {res}")
    return jsonify(result=res)

@app.route('/enable_all', methods=['POST'])
def enable_all():
    state = (request.form.get('state') == 'true')
    res = Bridge.call("set_enable_all", state)
    add_log(f"CMD: set_enable_all({state}) returned: {res}")
    return jsonify(result=res)

@app.route('/telemetry_global', methods=['POST'])
def telemetry_global():
    res = Bridge.call("toggle_telemetry")
    add_log(f"CMD: toggle_telemetry() returned: {res}")
    return jsonify(result=res)

@app.route('/telemetry_single', methods=['POST'])
def telemetry_single():
    mid = int(request.form.get('id', 1))
    res = Bridge.call("toggle_telemetry_single", mid)
    add_log(f"CMD: toggle_telemetry_single({mid}) returned: {res}")
    return jsonify(result=res)


def run_flask():
    app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)


is_setup = False
flask_started = False

def loop():
    global is_setup, flask_started
    if not is_setup:
        if not flask_started:
            t = threading.Thread(target=run_flask)
            t.daemon = True
            t.start()
            flask_started = True

        try:
            res = Bridge.call("ready")
            if res:
                add_log(f"Bridge ready response: {res}")
                print(res)
                is_setup = True
        except Exception as e:
            print(f"Waiting for bridge ready... {e}")
            time.sleep(1)
            return

    try:
        telemetry = Bridge.call("get_telemetry")
        if telemetry and isinstance(telemetry, str) and len(telemetry.strip()) > 0:
            lines = telemetry.strip().split('\n')
            for line in lines:
                if line.strip():
                    add_log(line)
    except Exception:
        pass

    time.sleep(1)

# Entry point
App.run(user_loop=loop)

Sketch side (sketch.ino)

#include "RollerMotor.h"
#include <Arduino_RouterBridge.h>

// Configurazione RS485
#define RS485_BAUD      115200
#define RS485_TIMEOUT   10

// --- DEFINIZIONE 4 MOTORI ---
RollerMotor m1(0x01, false); 
RollerMotor m2(0x02, true);  
RollerMotor m3(0x03, true);  
RollerMotor m4(0x04, false); 

// Variabili globali
unsigned long pollTick = 0;
bool isPolling = false;      
uint8_t rawBuffer[128];      
int bufIdx = 0;    
bool logM1=true, logM2=true, logM3=true, logM4=true;                  

bool isReady = false;


// ==========================================
// FUNZIONI HARDWARE
// ==========================================

String differentialDrive(int speed, int steering) {
  int speedL = constrain(speed + steering, -100, 100);
  int speedR = constrain(speed - steering, -100, 100);
  
  if (abs(speedL) < 3 && abs(speedR) < 3) {
      stop_all();
      return "Stopped due to low speed";
  }

  m1.setSpeed(speedL); delay(2);
  m4.setSpeed(speedL); delay(2); 
  m2.setSpeed(speedR); delay(2);
  m3.setSpeed(speedR);
  return "CMD: DRIVE L:" + String(speedL) + " R:" + String(speedR);
}

String move_single_motor(int id, int speed) {
  speed = constrain(speed, -100, 100);
  switch(id) {
    case 1: m1.setSpeed(speed); break;
    case 2: m2.setSpeed(speed); break;
    case 3: m3.setSpeed(speed); break;
    case 4: m4.setSpeed(speed); break;
  }

  return "CMD: M" + String(id) + " SPEED " + String(speed);
}

String set_enable_all(bool state) {
  m1.enable(state); 
  m2.enable(state); 
  m3.enable(state); 
  m4.enable(state);
  return "CMD: ALL MOTORS " + String(state ? "ENABLED" : "DISABLED");
}

String set_enable_single(int id, bool state) {
  switch(id) {
    case 1: m1.enable(state); break;
    case 2: m2.enable(state); break;
    case 3: m3.enable(state); break;
    case 4: m4.enable(state); break;
  }
  delay(5);
  return "CMD: M" + String(id) + (state ? " ENABLED" : " DISABLED");
}

String stop_all() {
  m1.setSpeed(0); delay(5);
  m2.setSpeed(0); delay(5);
  m3.setSpeed(0); delay(5);
  m4.setSpeed(0);
  return "CMD: STOP ALL";
}

String stop_single(int id) {
  move_single_motor(id, 0);
  return "CMD: STOP M" + String(id);
}

String toggle_telemetry() {
  isPolling = !isPolling;
  return "Telemetria Globale: " + String(isPolling ? "ON" : "OFF");
}

String toggle_telemetry_single(int id) {
  bool* flag = nullptr;
  switch(id) {
    case 1: flag = &logM1; break;
    case 2: flag = &logM2; break;
    case 3: flag = &logM3; break;
    case 4: flag = &logM4; break;
  }
  if (flag) {
    *flag = !(*flag);
    return "Log M" + String(id) + (*flag ? " ON" : " OFF");
  }
  return "Invalid motor ID";
}

String ready() {
  isReady = true;
  return "Sketch RollerMotor Controller Manuale pronto.\n"
         "Comandi:\n"
         "  DRIVE <speed> <steering>\n"
         "  MOTOR <id> <speed>\n"
         "  ENABLE ALL|<id>\n"
         "  DISABLE ALL|<id>\n"
         "  STOP ALL|<id>\n"
         "  TOGGLE TELEMETRY\n"
         "  TOGGLE TELEMETRY <id>\n";
}

String getAllTelemetry() {
  String out = "";
  if (logM1) out += printMotorInfo(m1) + "\n";
  if (logM2) out += printMotorInfo(m2) + "\n";
  if (logM3) out += printMotorInfo(m3) + "\n";
  if (logM4) out += printMotorInfo(m4) + "\n";
  return out;
}

void setup() {
  RS485.begin(RS485_BAUD);
  RS485.setTimeout(RS485_TIMEOUT);
  RS485.receive();

  m1.setSpeed(0); delay(5);
  m2.setSpeed(0); delay(5);
  m3.setSpeed(0); delay(5);
  m4.setSpeed(0); delay(5);

  m1.enable(true); delay(5);
  m2.enable(true); delay(5);
  m3.enable(true); delay(5);
  m4.enable(true); delay(5);

  // FIX BLOCCO AVVIO: Timeout di 500ms per svuotare il buffer
  unsigned long startFlush = millis();
  while (RS485.available() && (millis() - startFlush < 500)) {
    RS485.read();
  }

  // Inizializza Bridge
  Bridge.begin();
  
  // Attendi che Python sia pronto (max 30 secondi)
  delay(2000);
  
  // Registrazione Funzioni RPC
  Bridge.provide_safe("ready", ready);
  Bridge.provide_safe("get_telemetry", getAllTelemetry);
  Bridge.provide_safe("differentialDrive", differentialDrive);
  Bridge.provide_safe("move_single_motor", move_single_motor);
  Bridge.provide_safe("set_enable_single", set_enable_single);
  Bridge.provide_safe("stop_single", stop_single);
  Bridge.provide_safe("set_enable_all", set_enable_all);
  Bridge.provide_safe("stop_all", stop_all);
  Bridge.provide_safe("toggle_telemetry", toggle_telemetry);
  Bridge.provide_safe("toggle_telemetry_single", toggle_telemetry_single);

  delay(100);
}

void loop() {
  Bridge.update();
  delay(10);
}

// ==========================================
// LETTURA DATI
// ==========================================
void processIncomingData() {
  bufIdx = 0;
  unsigned long startRead = millis();
  
  while(millis() - startRead < 20) {
    if (RS485.available()) {
      if(bufIdx < 120) rawBuffer[bufIdx++] = RS485.read();
      startRead = millis(); 
    }
  }

  for(int i=0; i < bufIdx - 16; i++) {
    bool headerDetected = false;
    if (rawBuffer[i] == 0xAA && rawBuffer[i+1] == 0x55) headerDetected = true;
    else if (rawBuffer[i] == 0xAD && rawBuffer[i+1] == 0x05) headerDetected = true;
    else if (rawBuffer[i] == 0xB5 && rawBuffer[i+1] == 0x15) headerDetected = true;

    if (headerDetected) {
      uint8_t anchor = rawBuffer[i+2]; 
      RollerMotor* target = nullptr;
      bool shouldPrint = false;
      
      if (m1.getID() == 1 && (anchor == 0x15 || anchor == 0x50)) { target = &m1; shouldPrint = logM1; }
      if (m2.getID() == 2 && (anchor == 0x25 || anchor == 0x50)) { target = &m2; shouldPrint = logM2; }
      if (m3.getID() == 3 && (anchor == 0x35 || anchor == 0x50)) { target = &m3; shouldPrint = logM3; }
      if (m4.getID() == 4 && (anchor == 0x45 || anchor == 0x50)) { target = &m4; shouldPrint = logM4; }
      
      if (target != nullptr) {
        uint8_t cleanPacket[18];
        cleanPacket[0] = 0x50; cleanPacket[1] = target->getID();
        for(int k=0; k<16; k++) {
           if(i + 3 + k < bufIdx) cleanPacket[2+k] = rawBuffer[i+3+k];
        }
        target->parseResponse(cleanPacket);
        if (shouldPrint) {
           // Invio stringa su Bridge/Serial se possibile, o ignoro se solo polling
           // Serial.println(printMotorInfo(*target)); 
        }
        return; 
      }
    }
  }
}

String printMotorInfo(RollerMotor &m) {
  MotorTelemetry data = m.getData();
  String s = "M" + String(m.getID()) + " ";
  s += "V:" + String(data.speed, 1) + " ";
  s += "P:" + String(data.position, 0) + " ";
  s += "A:" + String(data.current);
  return s;
}

RollerMotor.h (library built by me to interface with rs485 protocol motors)

#ifndef ROLLER_MOTOR_H
#define ROLLER_MOTOR_H

#include <Arduino.h>
#include <ArduinoRS485.h>

struct MotorTelemetry {
    float speed;    float position; float current;
    uint8_t status; uint8_t error;  uint8_t mode;
};

class RollerMotor {
private:
    uint8_t _id;
    int8_t _dir; 
    MotorTelemetry _lastData;

    uint8_t calculateCRC8(const uint8_t* data, uint8_t len) {
        uint8_t crc = 0x00;
        while (len--) {
            crc ^= *data++;
            for (uint8_t i = 0; i < 8; i++) {
                if (crc & 0x01) crc = (crc >> 1) ^ 0x8C;
                else crc >>= 1;
            }
        }
        return crc;
    }

    void putLE32(uint8_t* p, int32_t v) {
        p[0] = (uint8_t)(v & 0xFF);
        p[1] = (uint8_t)((v >> 8) & 0xFF);
        p[2] = (uint8_t)((v >> 16) & 0xFF);
        p[3] = (uint8_t)((v >> 24) & 0xFF);
    }

    int32_t getLE32(uint8_t* p) {
        return (int32_t)(p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24));
    }

public:
    RollerMotor(uint8_t id, bool reverse = false) {
        _id = id;
        _dir = reverse ? -1 : 1;
        _lastData = {0, 0, 0, 0, 0, 0};
    }

    uint8_t getID() { return _id; }

    // Abilita (Lock) o Disabilita (Free) il motore
    void enable(bool on) {
        sendStandard(0x55, 1); delay(5);
        sendStandard(0x00, on ? 1 : 0);
    }

    // Imposta velocità (-100 a 100)
    // NOTA: Ho ripristinato maxCurrent a 1200000 (1.2A). 
    // Nel tuo codice era 120000 (120mA), che è troppo poco per frenare il robot!
    void setSpeed(float percent, int32_t maxCurrent = 1200000) {
        percent = constrain(percent, -100.0, 100.0);
        
        // 1. DEADBAND SOFTWARE
        // Se la richiesta è piccolissima (< 1%), forza a 0 assoluto.
        if (abs(percent) < 1.0) {
            percent = 0.0;
        }

        // 2. CALCOLO CORRETTO
        int32_t speedVal = (int32_t)(((percent * _dir) / 100.0f) * 19000.0f); 
        
        sendStandard(0x20, speedVal, maxCurrent);
    }

    // Funzione specifica per ARRESTO IMMEDIATO (Brake)
    // Manda velocità 0 ma con massima corrente disponibile per bloccare subito
    void brake() {
        setSpeed(0, 1500000); // Usa 1.5A per inchiodare la posizione
    }

    // Invia richiesta aggiornamento
    void requestUpdate() {
        uint8_t f[4] = {0x40, _id, 0, 0};
        f[3] = calculateCRC8(f, 3);
        RS485.beginTransmission();
        RS485.write(f, 4);
        RS485.endTransmission();
        RS485.receive();
    }

    void sendStandard(uint8_t cmd, int32_t d1, int32_t d2 = 0) {
        uint8_t f[15] = {0};
        f[0] = cmd; f[1] = _id;
        putLE32(&f[2], d1);
        putLE32(&f[6], d2);
        f[14] = calculateCRC8(f, 14);
        RS485.beginTransmission();
        RS485.write(f, 15);
        RS485.endTransmission();
        RS485.receive();
    }

    void parseResponse(uint8_t* buf) {
        _lastData.speed    = ((float)getLE32(&buf[2]) / 100.0f) * _dir;
        _lastData.position = ((float)getLE32(&buf[6]) / 100.0f) * _dir;
        _lastData.current  = (float)getLE32(&buf[10]) / 100.0f;
        _lastData.mode     = buf[14];
        _lastData.status   = buf[15];
        _lastData.error    = buf[16];
    }

    MotorTelemetry getData() { return _lastData; }
};

#endif

Give specific codes /exmples where call/provide and notify/provide do not work.

The code I've presented in the first Post is where I encounter the issue, I'll try tro produce more examples today and post them here, altought it is hard to reproduce the issue if i have no clue where it's coming from

I have exactly the same issues, currently AppLab is broken, try to upload your sketch by Arduino IDE

I am not using App Lab, but vs-code and the ssh-remote extension. running on app lab gives the same results, also, i can't run it on arduino IDE as i dont know how to run the python part on it

Quick experiment - could you try a longer delay in setup()?

Instead of:

// Attendi che Python sia pronto (max 30 secondi)
  delay(2000);

try:

// Attendi che Python sia pronto (max 30 secondi)
  delay(10000);

applied your change to the code.

result:

[main] Waiting for bridge ready... Request 'ready' failed: method ready not available (2)

Post the both sketch and script that you have uploaded from App Lab to check the readiness of MPU from MCU side.

fact is i didnt make a new sketch, i just modified my code where the user suggested to. my other sketches where i only hae one function exposed returning a string work flawlessly, i'm trying to understand why isnt my code working, not to get some other code working

To get help, you should be able to isolate your specific block of codes and post it that does not work to provide the expected result.

I'm also using vscode, just ran your App fully like usually, and then on top of it, just fire Arduino IDE and upload just sketch. It will work

i reproduced the issue with a smaller codebase

python:

import time

from arduino.app_utils import *

print("Hello world!")


def loop():
    start = time.time()
    response = Bridge.call("reverse", "Hello from Python!")
    end = time.time()
    print(f"Response: {response} (took {end - start:.4f}s)")
    start = time.time()
    response = Bridge.call("getMotorSpeed", 1)
    end = time.time()
    print(f"Response: {response} (took {end - start:.4f}s)")
    print(Bridge.call("setMotorSpeed", 1, 40))
    


# See: https://docs.arduino.cc/software/app-lab/tutorials/getting-started/#app-run
App.run(user_loop=loop)

sketch:

#include <Arduino.h>
#include <Arduino_RouterBridge.h>
#include "RollerMotor.h"

// --- DEFINIZIONE 4 MOTORI ---
RollerMotor m1(0x01, false); 
RollerMotor m2(0x02, true);  
RollerMotor m3(0x03, true);  
RollerMotor m4(0x04, false); 

String reverse(String original) {
  String reversed = "";
  for (int i = original.length() - 1; i >= 0; i--) {
    reversed += original[i];
  }
  return reversed;
}

String getMotorSpeed(int motorID) {
  MotorTelemetry data;

  if (motorID == 1) data = m1.getData();
  else if (motorID == 2) data = m2.getData();
  else if (motorID == 3) data = m3.getData();
  else if (motorID == 4) data = m4.getData();
  else return "Error: Invalid Motor ID";

  String s = "M" + String(motorID) + " ";
  s += "V:" + String(data.speed, 1) + " ";
  s += "P:" + String(data.position, 0) + " ";
  s += "A:" + String(data.current);
  return s;
}

String setMotorSpeed(int motorID, float speed) {
  if (motorID == 1) m1.setSpeed(speed);
  else if (motorID == 2) m2.setSpeed(speed);
  else if (motorID == 3) m3.setSpeed(speed);
  else if (motorID == 4) m4.setSpeed(speed);
  else return "Error: Invalid Motor ID";

  return "M" + String(motorID) + " SPEED SET TO " + String(speed);
}

void setup() {
  Bridge.begin();
  Bridge.provide("reverse", reverse);
  Bridge.provide("getMotorSpeed", getMotorSpeed);
  Bridge.provide("setMotorSpeed", setMotorSpeed);
}

void loop() {
}

the last function, the set speed function, breaks everything as soon as i provide it, when i provide it the other functions timeout, even the reverse function

if i comment the last Bridge.provide() the code works, the other functions work, it just break when i call the non provided function, but if i provide the speed function the other two break as well

Is it setMotorSpeed as given in the last Bridge.provide(); statement/declaration?

How have added RollerMotor.h library in the App Lab?

RollerMotor.h is in the same path as the sketch so it gets included that way, and yes the setMotorSpeed it the culprit provided function, that when 'provided' through the bridge

I cannot find the RoolerMotor.h in net. Kindly, forward to me the zip file so that I can include it in the IDE/sketch/AppLab.

Its code is provided in the first post

Please post the RollerMotor.zip file.

here's rollermotor.h
RollerMotor.zip (1,5 KB)

Try to upgrade AppLab to a new version and please report if problem was fixed

Hi @illmattep
I tested your code on core ver 0.52.0 and it should work.
I had to mock the set Speed method because I don't have anything on RS485, but I don't have any Bridge.provide issue