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