Talk to a native ROS 1 roscore directly — no ROS install, no bridge.
Python • C++ • Windows • Linux • macOS
A lightweight ROS client library that needs no ROS installed — and no bridge.
NoROSLib (you import it as irap_noroslib) speaks ROS 1's own wire protocol directly: it
registers with the ROS Master over XML-RPC and exchanges messages over
TCPROS / UDPROS — the very protocols rospy and roscpp use. So it connects
straight to a native, unmodified roscore, and real ROS nodes see it as a
legitimate ROS node. There is no rosbridge, no roslibpy/WebSocket gateway, and
nothing extra installed on the robot — and it links zero ROS libraries.
This is a real, wire-compatible ROS client — it publishes/subscribes real
topics, calls real services, runs real actions, and reads/writes real parameters
on a genuine roscore. It is not a rospy look-alike that only talks to other
NoROSLib programs. Available in Python (rospy-flavoured) and C++
(roscpp-flavoured, single-header); both are verified end-to-end against real ROS
Noetic — a real rostopic echo / rosservice call / rosparam sees it, and it
sees them.
Never used ROS before? Start at Step 1 — Install and follow the steps in order. The guide assumes you know nothing about ROS, and every step is a command you can copy and run.
Getting started (follow in order)
| Step | ||
|---|---|---|
| 0 | What ROS is, in 60 seconds | read this first if ROS is new to you |
| 1 | Install NoROSLib | Python: pip install. C++: copy one header. |
| 2 | Get a ROS master running | the "phone book" everything registers with |
| 3 | Point your program at the master | one environment variable |
| 4 | Check that it works | nr_rostopic list |
| 5 | Send data — your first publisher | Python + C++ |
| 6 | Receive data — your first subscriber | Python + C++ |
| 7 | Inspect the traffic | echo, hz, info, pub |
| 8 | Choose a message type | 64 built-ins |
| 9 | Use your robot's own message type | load a .msg file |
| 10 | Call a service | request → reply |
| 11 | Run an action | long job + feedback + cancel |
| 12 | Read and write parameters | shared config |
| 13 | Connect to a real robot on the network | the one gotcha that trips everyone |
| 14 | When something goes wrong | symptom → cause → fix |
Reference — Why NoROSLib · Capabilities ·
Custom messages in depth ·
nr_rostopic ·
nr_roscore ·
Automatic md5 discovery ·
All examples · How it works
Skip this if you already know ROS. If you don't, these seven words are all you need to follow the rest of the guide.
| Word | What it actually means |
|---|---|
| Node | One running program. Your program will be a node. |
Master (roscore) |
A phone book. Nodes register with it and ask it "who has /chatter?" It stores addresses, not your data. Exactly one runs per system. |
| Topic | A named channel, like /chatter or /camera/image_raw. Data flows one way, and any number of nodes can send or receive on it. |
| Message | The data itself. Every message has a type, like std_msgs/String (a piece of text) or sensor_msgs/Image (a camera frame). |
| Publish / subscribe | Publish = send on a topic. Subscribe = receive from a topic. Publishers and subscribers don't know each other; they only know the topic name. |
| Service | A request → reply call, like calling a function on another node. Returns immediately. |
| Action | A long-running job (drive somewhere, pick something up) that reports progress while it runs and can be cancelled. |
One more thing, and it's the thing beginners get stuck on:
The master only introduces nodes; it does not carry their data. When your node subscribes to
/chatter, the master tells it "the publisher is at 192.168.1.50:41001" — and then your node opens a direct connection to that publisher. Data never flows through the master. This is why, in Step 13, the robot has to be able to reach your machine back, not just the other way round.
You do not need ROS installed. Not now, not later.
First get the code. This is currently the only way to install it — NoROSLib is
not on PyPI yet, so pip install irap_noroslib will not work:
git clone https://github.com/IRAP-Organization/NoROSLib.git
cd NoROSLibNow pick your language.
Install from the python/ folder of the clone — note the cd, and the .
(a dot, meaning "this folder"):
cd python
pip install .That's it. If pip isn't found, try pip3 install ., and if you want to edit the
library and see your changes immediately, use pip install -e ..
Once NoROSLib is published to PyPI,
pip install irap_noroslibwill work from anywhere and you can skip the clone. It is not published yet.
Dependencies: none. It is pure Python 3.6+ standard library — it talks to
ROS using only xmlrpc, socket, socketserver, threading, struct and
hashlib, all built into Python itself.
python/requirements.txt is intentionally empty and
says exactly that. (OpenCV/numpy are needed only by the optional webcam demos,
never by the library.)
Check it worked:
python3 -c "import irap_noroslib; print('ok')"Installing also puts two commands on your PATH — you will use them in Step 2 and Step 4:
nr_roscore --help # your own ROS master
nr_rostopic --help # rostopic, without ROSThere is nothing to install. The whole library is one file inside the clone:
cpp/include/irap_noroslib.hpp. Copy it next to
your code and you are done:
# from inside the NoROSLib clone
cp cpp/include/irap_noroslib.hpp ~/my_project/In a single-file program, ask for the implementation once, at the top:
#define IRAP_NOROSLIB_IMPLEMENTATION // exactly one .cpp in your project does this
#include "irap_noroslib.hpp"Compile with a C++17 compiler and threads:
g++ -std=c++17 my_node.cpp -o my_node -pthreadDependencies: none beyond a C++17 toolchain. No ROS, no Boost, no third-party
libraries — just the compiler and the OS's own sockets/threads. On Linux/macOS/WSL
pass -pthread; on MinGW add -lws2_32 (MSVC links Winsock automatically). Full
details in cpp/DEPENDENCIES.md.
Bigger project (more than one
.cpp)? Put those two lines in exactly one.cppfile; every other file just does#include "irap_noroslib.hpp". That is the whole rule.
Optional: if you'd rather write ROS-style includes (
#include "irap_noroslib/std_msgs/String.h"), copy the smallcpp/include/irap_noroslib/folder too. Those are one-line convenience headers;irap_noroslib.hppalone already contains every type.
Every ROS system needs exactly one master running somewhere. Two situations:
Then the master is already up over there — someone ran roscore on it. Do
nothing here, find its IP address, and go to
Step 3.
Run NoROSLib's own master. It is a real ROS master: real ROS nodes,
rostopic/rosservice/rosparam, and NoROSLib nodes all register with it.
nr_roscore # port 11311, all interfaces
nr_roscore --bind 127.0.0.1 --port 11311 # this machine onlyC++ users who built the examples can run ./cpp/build/nr_roscore --port 11311
instead — same thing.
Leave it running in its own terminal. You should see — and it tells you the exact line to paste into the next step:
[nr_roscore] ROS master online
[nr_roscore] ROS_MASTER_URI=http://127.0.0.1:11311/ (bind 127.0.0.1:11311)
[nr_roscore] point nodes here: export ROS_MASTER_URI=http://127.0.0.1:11311/
[nr_roscore] started /rosout aggregator (-> /rosout_agg)
Every other step in this guide assumes a master is up. If nothing works later, come back and check this terminal is still alive.
A node needs to know two things, and you should set both in your code:
| What it means | |
|---|---|
| master URI | Where the master is — the phone book you register with. |
| hostname | How other nodes reach you back. Remember Step 0: data goes node-to-node, so peers must be able to open a connection to you. |
Two lines, before init_node(). Everything on one machine? Both are localhost:
import irap_noroslib
irap_noroslib.set_master_uri("http://localhost:11311") # where the master is
irap_noroslib.set_hostname("localhost") # how peers reach me
irap_noroslib.init_node("my_node") # now join the graph#include "irap_noroslib.hpp"
irap_noroslib::set_master_uri("http://localhost:11311"); // where the master is
irap_noroslib::set_hostname("localhost"); // how peers reach me
irap_noroslib::init_node("my_node"); // now join the graphWhy in the file and not the shell? Because what you set in code wins over the
environment, and the environment is where things go wrong. Any machine that has
ever had ROS on it tends to carry a stale ROS_MASTER_URI, ROS_IP or
ROS_HOSTNAME in its shell profile, left over from some other robot. Your node
silently inherits it, advertises an address nobody can reach, and then nothing
connects and nothing tells you why — no error, no message, just silence. Setting
these two values in the file makes your program immune to that, and it behaves the
same on every machine you copy it to.
Precedence, in both languages — the first one that exists wins:
init_node(name, master_uri, host) > set_master_uri() / set_hostname() > $ROS_MASTER_URI / $ROS_IP > $ROS_HOSTNAME > auto-detect
So you can also pass them straight to init_node, which is the same thing in one
line:
irap_noroslib.init_node("my_node", "http://localhost:11311", "localhost")irap_noroslib::init_node("my_node", "http://localhost:11311", "localhost");Standard ROS uses environment variables, and NoROSLib reads them too, so this works if you leave the two lines out:
export ROS_MASTER_URI=http://localhost:11311 # where the master is
export ROS_IP=localhost # how peers reach youBut you must repeat it in every terminal, and a leftover value from an old
setup silently overrides your intent. Use it for quick one-off shell commands
(nr_rostopic reads $ROS_MASTER_URI too); set it in the file for anything
you intend to keep. If you do rely on the shell, check what is already there
before you start:
echo "$ROS_MASTER_URI $ROS_IP $ROS_HOSTNAME" # all empty? then nothing can surprise youBefore writing any code, confirm your terminal can see the master:
nr_rostopic listExpected output (a bare master with nothing else running):
/rosout
/rosout_agg
That's success — those two topics always exist. If instead you get
connection refused, the master isn't running or ROS_MASTER_URI is wrong: go
back to Step 2 /
Step 3.
Pointing at the master explicitly works too, if you'd rather not use the variable:
nr_rostopic --master 192.168.1.50 --port 11311 listYou will publish text on the topic /chatter, ten times a second. This is the
"hello world" of ROS.
Save as my_talker.py:
import irap_noroslib
from irap_noroslib.std_msgs.msg import String # a message type: plain text
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3: where the master is
irap_noroslib.set_hostname("localhost") # Step 3: how peers reach me
irap_noroslib.init_node("my_talker") # join the graph, pick a node name
pub = irap_noroslib.Publisher("/chatter", String) # I will send String on /chatter
rate = irap_noroslib.Rate(10) # 10 times per second
i = 0
while not irap_noroslib.is_shutdown(): # until Ctrl-C
pub.publish(String(data="hello world %d" % i))
i += 1
rate.sleep() # keeps the 10 Hz pacepython3 my_talker.pySave as my_talker.cpp:
#define IRAP_NOROSLIB_IMPLEMENTATION
#include "irap_noroslib.hpp"
int main() {
irap_noroslib::set_master_uri("http://localhost:11311"); // Step 3: where the master is
irap_noroslib::set_hostname("localhost"); // Step 3: how peers reach me
irap_noroslib::init_node("my_talker"); // join the graph
irap_noroslib::Publisher<std_msgs::String> pub("/chatter");
irap_noroslib::Rate rate(10); // 10 times per second
int i = 0;
while (irap_noroslib::ok()) { // until Ctrl-C
std_msgs::String m;
m.data = "hello world " + std::to_string(i++);
pub.publish(m);
rate.sleep();
}
}g++ -std=c++17 my_talker.cpp -o my_talker -pthread
./my_talkerIt looks like it's doing nothing. It isn't — nobody is listening yet. Leave it running and open a new terminal.
The same program ships ready-made as
python/examples/talker.pyandcpp/examples/talker.cpp.
In a new terminal (remember export ROS_MASTER_URI=... again — every terminal
needs it), listen to what the talker is sending.
Save as my_listener.py:
import irap_noroslib
from irap_noroslib.std_msgs.msg import String
def on_message(m): # called for every message
irap_noroslib.loginfo("I heard: " + m.data)
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3: where the master is
irap_noroslib.set_hostname("localhost") # Step 3: how peers reach me
irap_noroslib.init_node("my_listener")
irap_noroslib.Subscriber("/chatter", String, on_message)
irap_noroslib.spin() # wait for messages foreverpython3 my_listener.pySave as my_listener.cpp:
#define IRAP_NOROSLIB_IMPLEMENTATION
#include "irap_noroslib.hpp"
int main() {
irap_noroslib::set_master_uri("http://localhost:11311"); // Step 3: where the master is
irap_noroslib::set_hostname("localhost"); // Step 3: how peers reach me
irap_noroslib::init_node("my_listener");
irap_noroslib::Subscriber<std_msgs::String> sub("/chatter",
[](const std_msgs::String& m) { // called per message
irap_noroslib::loginfo("I heard: " + m.data);
});
irap_noroslib::spin(); // wait forever
}g++ -std=c++17 my_listener.cpp -o my_listener -pthread
./my_listenerYou should now see, ten times a second:
[INFO] [1784011311.460142]: I heard: hello world 245
[INFO] [1784011311.560102]: I heard: hello world 246
That is a working ROS system. The two mix freely: a Python talker feeds a
C++ listener, and either one feeds a real rospy/roscpp node on a robot —
they are all just ROS nodes on the wire.
nr_rostopic is the real rostopic tool, with no ROS installed — same
subcommands, same flags. With the talker from Step 5 still running:
nr_rostopic list # what topics exist? -> /chatter ...
nr_rostopic echo /chatter # print the messages (Ctrl-C to stop)
nr_rostopic echo -n 5 /chatter # print 5, then exit
nr_rostopic type /chatter # what type is it? -> std_msgs/String
nr_rostopic info /chatter # who publishes / subscribes?
nr_rostopic hz /chatter # how fast? -> ~10 Hz
nr_rostopic bw /chatter # how many bytes/s?echo prints:
data: "hello world 25"
---
data: "hello world 26"
---
You can also send a message by hand — handy for testing a subscriber with no publisher written yet:
nr_rostopic pub -r 10 /chatter std_msgs/String "data: hi" # 10 Hz
nr_rostopic pub -1 /chatter std_msgs/String "data: hi" # onceFull reference: nr_rostopic —
including the one thing it does that real rostopic cannot.
nr_rosservice is the real rosservice tool, with no ROS installed — same
subcommands, same flags. With a service running (say /add_two_ints):
nr_rosservice list # what services exist?
nr_rosservice type /add_two_ints # what type is it? -> rospy_tutorials/AddTwoInts
nr_rosservice uri /add_two_ints # its ROSRPC uri
nr_rosservice info /add_two_ints # node, uri, type, args
nr_rosservice find rospy_tutorials/AddTwoInts # services of a type
nr_rosservice call /add_two_ints "a: 3, b: 4" # call it -> sum: 7type/uri/info/find work for any service — the master doesn't record a
service's type, so nr_rosservice probes the running service directly, exactly
as rosservice does. args/call need the request's field layout, which a
service does not send over the wire: built-ins (std_srvs) and anything you
loaded with load_srv_file() just work; for anything else, point it at the file:
nr_rosservice call -f my_pkg/srv/AddTwoInts.srv /add_two_ints "a: 3, b: 4"It shares ./master.yaml with nr_rostopic, so --set_ros_master_uri set once
serves both tools.
nr_rosnode is the real rosnode tool, with no ROS installed — same
subcommands, same flags:
nr_rosnode list [-a] # active nodes (with -a: name + API uri)
nr_rosnode info /talker # publications, subscriptions, services, pid, connections
nr_rosnode ping -c 3 /talker # test connectivity (round-trip time)
nr_rosnode machine [HOST] # list machines, or nodes on one machine
nr_rosnode kill /talker # ask a node to shut down
nr_rosnode cleanup # purge dead nodes' stale registrations from the masterNode names and their topics/services come from the master; pid, connections,
liveness and shutdown come from calling each node's own slave API directly —
exactly as rosnode does. It shares ./master.yaml too.
nr_rosbag is the real rosbag tool, with no ROS installed — record, play
and info, writing genuine v2.0 bags:
nr_rosbag record -a -O run.bag # record all topics (Ctrl-C to stop)
nr_rosbag record /chatter /cmd_vel # or named topics
nr_rosbag record -a --split --duration 60 # roll to a new bag every 60s
nr_rosbag info run.bag # summarize a bag (or several)
nr_rosbag play run.bag other.bag # replay, merged in time order
nr_rosbag play -r 2 -l run.bag # 2× speed, looprecord captures the exact wire bytes of any topic — even a custom type you
have no .msg file for — because it saves the publisher's message_definition
alongside, exactly as real rosbag does. The bags are the real format, so they
open in genuine rosbag info / rosbag play / rqt_bag, and a bag recorded by
real ROS plays back here. Verified both ways against ROS Noetic.
std_msgs/String was just text. Real robots send poses, scans, images. 64 types
are built in — the standard ROS catalog, every md5sum matching rosmsg md5
exactly, so real ROS nodes accept them.
| Package | Types |
|---|---|
| std_msgs (19) | Bool, Byte, Char, ColorRGBA, Duration, Empty, Float32, Float64, Header, Int8, Int16, Int32, Int64, String, Time, UInt8, UInt16, UInt32, UInt64 |
| geometry_msgs (16) | Accel, Point, Point32, Polygon, Pose, PoseArray, PoseStamped, PoseWithCovariance, Quaternion, Transform, TransformStamped, Twist, TwistStamped, TwistWithCovariance, Vector3, Wrench |
| sensor_msgs (14) | CameraInfo, CompressedImage, Image, Imu, JointState, LaserScan, MagneticField, NavSatFix, NavSatStatus, PointCloud2, PointField, Range, RegionOfInterest, Temperature |
| nav_msgs (5) | GridCells, MapMetaData, OccupancyGrid, Odometry, Path |
| diagnostic_msgs (3) | DiagnosticArray, DiagnosticStatus, KeyValue |
| trajectory_msgs (4) | JointTrajectory, JointTrajectoryPoint, MultiDOFJointTrajectory, MultiDOFJointTrajectoryPoint |
| actionlib_msgs (3) | GoalID, GoalStatus, GoalStatusArray |
Use one exactly like String. Driving a robot, for instance, is a
geometry_msgs/Twist on /cmd_vel:
import irap_noroslib
from irap_noroslib.geometry_msgs.msg import Twist
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3
irap_noroslib.set_hostname("localhost")
irap_noroslib.init_node("driver")
pub = irap_noroslib.Publisher("/cmd_vel", Twist)
cmd = Twist()
cmd.linear.x = 0.2 # 0.2 m/s forward (nested fields: linear.x)
cmd.angular.z = 0.5 # 0.5 rad/s turning
pub.publish(cmd)#define IRAP_NOROSLIB_IMPLEMENTATION
#include "irap_noroslib.hpp"
int main() {
irap_noroslib::set_master_uri("http://localhost:11311"); // Step 3
irap_noroslib::set_hostname("localhost");
irap_noroslib::init_node("driver");
irap_noroslib::Publisher<geometry_msgs::Twist> pub("/cmd_vel");
geometry_msgs::Twist cmd;
cmd.linear.x = 0.2; // 0.2 m/s forward
cmd.angular.z = 0.5; // 0.5 rad/s turning
pub.publish(cmd);
}The Python and C++ catalogs are identical, all 64 types — same names, same md5s. Details and more usage: python/README.md, cpp/README.md.
Type you need isn't in the list? That's the next step.
Robots define their own message types (my_robot_msgs/CustomData). You do not
need their catkin package, and you don't need ROS. You need the .msg file.
1. Copy the .msg files off the robot — scp, a USB stick, anything. Put them
anywhere:
/home/me/robot_msgs/Reading.msg
/home/me/robot_msgs/CustomData.msg
2. Load each file by its full path. One file, one call:
import irap_noroslib
from irap_noroslib import load_msg_file
MSGS = "/home/me/robot_msgs" # wherever you put them
PKG = "my_robot_msgs" # the ROS package they came from
Reading = load_msg_file(f"{MSGS}/Reading.msg", PKG)
CustomData = load_msg_file(f"{MSGS}/CustomData.msg", PKG)
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3
irap_noroslib.set_hostname("localhost")
irap_noroslib.init_node("my_node")
irap_noroslib.Subscriber("/data", CustomData, lambda m: print(m.id, m.label))
irap_noroslib.spin()#define IRAP_NOROSLIB_IMPLEMENTATION
#include "irap_noroslib.hpp"
using namespace irap_noroslib;
const std::string MSGS = "/home/me/robot_msgs";
const std::string PKG = "my_robot_msgs";
int main() {
MsgType Reading = load_msg_file(MSGS + "/Reading.msg", PKG);
MsgType CustomData = load_msg_file(MSGS + "/CustomData.msg", PKG);
set_master_uri("http://localhost:11311"); // Step 3
set_hostname("localhost");
init_node("my_node");
DynamicSubscriber sub("/data", CustomData, [](const DynamicMessage& m) {
loginfo(m.get<std::string>("label")); // fields by name
});
spin();
}That's it. NoROSLib derives the md5sum and the wire format from the file using the exact algorithm real ROS uses, so the robot accepts your node as native.
Three rules, and they cover everything that can go wrong here:
- One call per file.
CustomDatanestsReading, soReading.msggets loaded too. Order doesn't matter. Forget one and the error names the file you missed. - Built-in types need no call.
std_msgs/Header,geometry_msgs/Point, … all 64 are already there. - Get the package name right. It isn't in the md5, so a typo won't break the
connection — it will quietly make
rostopic echofail androsbagrecord the wrong type. Details: Does the package name matter?
Services (.srv) and actions (.action) load the same way, with
load_srv_file() and load_action_file(). Full detail — including defining a
type in code with no file at all — in
Custom messages in depth. Working example:
custom_msg (both languages).
No
.msgfile either? You can still read the topic:nr_rostopic echodecodes a type it has never seen, because ROS publishers hand over their full message definition when they connect. Seeechodoes something realrostopiccannot.
A topic is a stream. A service is a request → reply call, like a function on
another node. Example: add_two_ints (a + b → sum).
A service type is just two message bodies: the request, then the response.
Describe it once and NoROSLib derives the md5 (matching rossrv md5). If you have
the robot's .srv file, use load_srv_file(path, pkg) instead — same result,
see Step 9.
The server — waits for requests:
import irap_noroslib
from irap_noroslib import define_service
AddTwoInts = define_service("rospy_tutorials/AddTwoInts",
"int64 a\nint64 b", # request fields
"int64 sum") # response fields
def handle(req): # called per request
return AddTwoInts.Response(sum=req.a + req.b)
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3
irap_noroslib.set_hostname("localhost")
irap_noroslib.init_node("add_server")
irap_noroslib.Service("/add_two_ints", AddTwoInts, handle)
irap_noroslib.spin()The client — asks, and waits for the answer:
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3
irap_noroslib.set_hostname("localhost")
irap_noroslib.init_node("add_client")
irap_noroslib.wait_for_service("/add_two_ints", timeout=5.0) # let the server come up
add = irap_noroslib.ServiceProxy("/add_two_ints", AddTwoInts)
resp = add(AddTwoInts.Request(a=3, b=4)) # blocks until the server replies
print("sum =", resp.sum) # sum = 7AddTwoInts here is a small typed struct — see cpp/examples/add_two_ints.hpp
(or load the .srv file and use DynamicServiceServer / DynamicServiceClient).
// server
irap_noroslib::set_master_uri("http://localhost:11311"); // Step 3
irap_noroslib::set_hostname("localhost");
irap_noroslib::init_node("add_server");
irap_noroslib::ServiceServer<AddTwoInts> srv("/add_two_ints",
[](const AddTwoInts::Request& req, AddTwoInts::Response& resp) {
resp.sum = req.a + req.b;
return true; // false = the call failed
});
irap_noroslib::spin();
// client
irap_noroslib::set_master_uri("http://localhost:11311"); // Step 3
irap_noroslib::set_hostname("localhost");
irap_noroslib::init_node("add_client");
irap_noroslib::ServiceClient<AddTwoInts> client("/add_two_ints");
AddTwoInts::Request req; req.a = 3; req.b = 4;
AddTwoInts::Response resp;
if (client.call(req, resp)) // blocks until the server replies
irap_noroslib::loginfo("sum = " + std::to_string(resp.sum));Run the ready-made pair, server first:
python3 python/examples/add_two_ints_server.py # terminal 1
python3 python/examples/add_two_ints_client.py # terminal 2
./cpp/build/add_two_ints_server # or the C++ pair
./cpp/build/add_two_ints_clientBoth work against real ROS: a real rosservice call /add_two_ints "a: 3 b: 4"
reaches your server, and your client calls a real rospy service.
An action is for a job that takes time — drive to the kitchen, pick that up. Unlike a service it sends feedback while it runs and can be cancelled. The classic example computes a Fibonacci sequence one step at a time.
python3 python/examples/fibonacci_server.py # terminal 1: does the work
python3 python/examples/fibonacci_client.py # terminal 2: sends the goal
./cpp/build/fibonacci_server # the same pair in C++
./cpp/build/fibonacci_clientThe client sends a goal, gets feedback on every step, and finally a
result plus a state (3 = SUCCEEDED):
[INFO] feedback: [0, 1, 1, 2, 3]
[INFO] feedback: [0, 1, 1, 2, 3, 5]
[INFO] feedback: [0, 1, 1, 2, 3, 5, 8]
[INFO] state=3 (3=SUCCEEDED) result=[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
These interoperate with real actionlib: your client drives a real ROS action
server, and a real ROS client (or axclient.py) drives your server. Read
fibonacci_client.py / .cpp — they are short — and copy the shape.
Parameters are shared settings kept on the master — a robot's name, a speed limit, a camera resolution. Any node can read or write them.
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3
irap_noroslib.set_hostname("localhost")
irap_noroslib.init_node("params") # parameters live on the master
irap_noroslib.set_param("/robot_name", "turtle")
irap_noroslib.set_param("/max_speed", 1.5)
name = irap_noroslib.get_param("/robot_name") # "turtle"
speed = irap_noroslib.get_param("/max_speed", 1.0) # 1.0 if unset (a default)
if irap_noroslib.has_param("/max_speed"):
irap_noroslib.delete_param("/max_speed")irap_noroslib::set_master_uri("http://localhost:11311"); // Step 3
irap_noroslib::set_hostname("localhost");
irap_noroslib::init_node("params"); // parameters live on the master
irap_noroslib::set_param("/robot_name", "turtle");
irap_noroslib::set_param("/max_speed", 1.5);
// get_param_or<T>(key, fallback) -- returns the fallback if unset or wrong type
std::string name = irap_noroslib::get_param_or<std::string>("/robot_name", "unknown");
double speed = irap_noroslib::get_param_or<double>("/max_speed", 1.0);
if (irap_noroslib::has_param("/max_speed")) irap_noroslib::delete_param("/max_speed");From the shell (real ROS tools work against NoROSLib and vice-versa):
rosparam set /max_speed 1.5
rosparam get /robot_nameFull example: params_example in both languages.
Everything so far ran on one machine, so both values were localhost. Now the
master is on the robot and your code is on your laptop. Change the same two
lines from Step 3 — the master URI now
points at the robot, and the hostname becomes your laptop's real IP:
irap_noroslib.set_master_uri("http://192.168.1.50:11311") # the ROBOT's master
irap_noroslib.set_hostname("192.168.1.77") # YOUR laptop's IP
irap_noroslib.init_node("my_node")irap_noroslib::set_master_uri("http://192.168.1.50:11311"); // the ROBOT's master
irap_noroslib::set_hostname("192.168.1.77"); // YOUR laptop's IP
irap_noroslib::init_node("my_node");Why the hostname matters. Remember from Step 0:
the master only hands out addresses — the actual data goes node to node,
directly. So when you subscribe, the robot's publisher has to open a connection
back to your laptop, and it uses the address you registered with. Leave it
unset and NoROSLib falls back to your machine's hostname — which the robot usually
cannot resolve (my-laptop.local), or to a stale $ROS_IP from your shell.
Symptom: the topic appears in nr_rostopic list, but no messages ever arrive.
Give it the IP the robot can actually reach you on, and it works.
Find your IP with ip addr (Linux/macOS) or ipconfig (Windows) — it's the one
on the same subnet as the robot, here 192.168.1.x.
Checklist, in order:
- Ping both ways.
ping 192.168.1.50from the laptop, andping 192.168.1.77from the robot. Both must work. One-way is exactly the failure an unreachable hostname causes, so prove it now. set_master_uri("http://<robot-ip>:11311")in your file.set_hostname("<your-laptop-ip>")in your file.nr_rostopic list— you should see the robot's real topics. (This is a shell command, so give it the master on the command line:nr_rostopic --master 192.168.1.50 list.)nr_rostopic --master 192.168.1.50 echo /some_topic— data should flow. If step 4 works but this one hangs, it is the hostname or a firewall blocking the robot from connecting back to you.
Firewalls: ROS uses port 11311 for the master and random high ports for the node-to-node connections. On a trusted lab network, allow the robot's IP through your firewall entirely; don't try to pin the ports.
Nothing needs to be installed on the robot. It sees a normal ROS node.
| What you see | What it means | Fix |
|---|---|---|
connection refused on any command |
No master at that address. | Is nr_roscore/roscore running? Is ROS_MASTER_URI right? (Step 2) |
nr_rostopic list shows only /rosout, /rosout_agg |
You're talking to the master fine, but no nodes are publishing. | Start the talker. If it is running, it's pointed at a different master — check ROS_MASTER_URI in that terminal. |
Topic is listed, but echo prints nothing |
The publisher can't connect back to you. | set_hostname("<your IP>") in your file; open the firewall. (Step 13) |
| Everything hangs / a service call times out, even with everything on one machine | Your shell has a leftover ROS_IP/ROS_HOSTNAME/ROS_MASTER_URI from an old ROS setup, and your node silently inherited it — so it advertises an address peers cannot reach. |
set_master_uri(...) + set_hostname("localhost") in the file — code beats the environment. (Step 3) |
Cannot load message class for [pkg/Type] from real rostopic |
Real ROS needs that type built in a catkin package. Not your bug. | Use nr_rostopic echo — it decodes types it has no file for. (here) |
unknown message type "pkg/Thing" from NoROSLib |
A nested custom type whose .msg you didn't load. |
load_msg_file() that file too — the error names it. (Step 9) |
| md5sum mismatch, publisher drops you | Your subscriber's type doesn't match the publisher's. | Nothing to do — NoROSLib reads the real md5 out of the error and reconnects. (md5 discovery) |
| Real ROS subscribers ignore what you publish | You advertised a wrong md5 (hand-written C++ struct). | Load the .msg file instead and let it derive the md5. (Step 9) |
C++: undefined reference to irap_noroslib::... |
You never compiled the implementation. | #define IRAP_NOROSLIB_IMPLEMENTATION in exactly one .cpp. (Step 1) |
C++: undefined reference to pthread_* |
Missing the threads flag. | Add -pthread (and -lws2_32 on MinGW). |
| Node names collide / one node dies on startup | Two nodes registered with the same name. | Give each init_node() a unique name. |
While debugging, keep the default log level ("info") — it prints the master URI,
every connection, and every md5 rejection, which is usually the whole answer. All
of it goes to stderr, so it never pollutes a piped nr_rostopic echo.
Once things work, you can quieten the library — "info" (default), "warn",
"error", "none":
irap_noroslib.set_log_level("warn")irap_noroslib::set_log_level("warn");ROS Noetic officially supports only Ubuntu 20.04. That is a real problem the moment your world isn't exactly Ubuntu 20.04:
- Your OS is newer (or different). Run your node on Ubuntu 22.04 — or any distro that isn't 20.04 — and the prebuilt ROS Noetic packages simply aren't there for you.
- Building ROS from source is painful and slow. Compiling ROS Noetic yourself on an unsupported OS takes a very long time, drags in a mountain of dependencies, and often still breaks.
- Windows is effectively a no-go. Getting ROS Noetic onto Windows is a huge, time-consuming effort — if you get it working at all.
NoROSLib sidesteps all of it. Instead of installing ROS, it just talks to a ROS Noetic master over the network the same way a real node does. So a program on Ubuntu 22.04, an older/newer distro, or any machine with a socket can join a ROS Noetic graph — publish/subscribe topics, call services, run actions, read and write parameters — without building or installing ROS at all.
NoROSLib is not rosbridge/roslibpy, and it does not need one. It runs no
rosbridge_server, no WebSocket gateway, no translator process — on either side.
It implements the ROS 1 protocols themselves (XML-RPC master/slave discovery +
TCPROS/UDPROS transport), so from the robot's point of view a NoROSLib process is
indistinguishable from a native roscpp / rospy node. Point it at the robot's
ROS_MASTER_URI and go.
| Approach | Bridge/server needed on the ROS side? | Speaks native ROS wire protocol? | Extra install on the robot |
|---|---|---|---|
| rosbridge + roslibpy | Yes — rosbridge_server |
No — JSON over WebSocket | rosbridge_suite |
| NoROSLib | No | Yes — XML-RPC + TCPROS/UDPROS | None |
your machine (Windows / macOS / any Linux) the robot (Ubuntu + ROS)
┌──────────────────────────────┐ ┌────────────────────────┐
│ your app + irap_noroslib │ XML-RPC + │ roscore │
│ (no ROS, no bridge) │◄── TCPROS ───►│ native rospy/roscpp │
└──────────────────────────────┘ (real ROS) │ nodes (unmodified) │
└────────────────────────┘
| Runs on | |
|---|---|
| Python | Pure standard-library — anywhere Python 3.6+ runs (Linux, macOS, Windows). |
| C++ | Linux, macOS, WSL, and native Windows — a built-in Winsock/POSIX compatibility layer picks the right socket API per platform. On Windows link ws2_32 (MSVC auto-links it; with MinGW pass -lws2_32). |
The master it talks to is a normal ROS Noetic roscore (typically on an
Ubuntu 20.04 box, a container, or a robot) — NoROSLib is the client that reaches
it from elsewhere.
| Feature | Transport / API | Python | C++ |
|---|---|---|---|
| Topics (pub/sub) | TCPROS and UDPROS | ✅ | ✅ |
| Services (srv) | rosrpc / TCPROS | ✅ | ✅ |
| Actions (actionlib) | 5 action topics | ✅ | ✅ |
| Parameters | master XML-RPC | ✅ | ✅ |
| Messages | std/geometry/sensor/nav/diagnostic/trajectory + custom | ✅ | ✅ |
Load .msg/.srv/.action files |
point it at a file copied off a robot | ✅ | ✅ |
| md5 | auto-computed and auto-discovered from mismatch | ✅ | ✅ |
| Config | master URI + hostname from code | ✅ | ✅ |
nr_rostopic |
list / echo / pub / info / hz / bw / find — echo works on types with no .msg |
✅ | ✅ |
Master (nr_roscore) |
be the roscore: Master + Param Server (dict trees) + /rosout |
✅ | ✅ |
Message imports follow the same paths rospy and roscpp use, just prefixed — porting a node is a one-word edit:
from std_msgs.msg import String # rospy
from irap_noroslib.std_msgs.msg import String # irap_noroslib#include "std_msgs/String.h" // roscpp
#include "irap_noroslib/std_msgs/String.h" // irap_noroslib -> still std_msgs::StringStep 9 covers the common case. Here is
the whole picture — your own message type, three ways. All three produce the
same wire bytes and the same md5sum, so they interoperate freely with each
other and with real ROS. Example: custom_msg (shows two of them side by side).
| You give it | md5sum | Both languages? | |
|---|---|---|---|
1. Load the .msg file |
the file's path | derived for you | ✅ |
2. From .msg text, in code |
the text | derived for you | Python |
| 3. A hand-written struct | fields + codec + the md5 | you supply it | C++ |
Copy the definition files off the robot and hand NoROSLib the full path of each one. No catkin package, no ROS install, nothing to write.
Say you scp'd three files off a robot running the package my_robot_msgs, and
dropped them anywhere — there is no package.xml, no msg/ directory, they are
just files:
/home/me/robot_msgs/Reading.msg
/home/me/robot_msgs/CustomData.msg
/home/me/robot_msgs/GetStatus.srv
# /home/me/robot_msgs/CustomData.msg
std_msgs/Header header
int32 id
string label
float64[] samples
Reading[] readings <-- a custom type from the same package
geometry_msgs/Point where <-- a built-in; nothing to do, it just resolves
Load each file by its own full path. One file, one call:
import irap_noroslib
from irap_noroslib import load_msg_file, load_srv_file
MSGS = "/home/me/robot_msgs" # wherever you put them
PKG = "my_robot_msgs" # the package they came from
Reading = load_msg_file(f"{MSGS}/Reading.msg", PKG)
CustomData = load_msg_file(f"{MSGS}/CustomData.msg", PKG)
GetStatus = load_srv_file(f"{MSGS}/GetStatus.srv", PKG)
irap_noroslib.set_master_uri("http://localhost:11311") # Step 3
irap_noroslib.set_hostname("localhost")
irap_noroslib.init_node("my_node")
# publish it
pub = irap_noroslib.Publisher("/data", CustomData)
m = CustomData(id=7, label="hi", samples=[1.0, 2.0])
m.header.frame_id = "base_link"
m.readings = [Reading(value=1.5, unit="C")]
m.where.x = 3.0
pub.publish(m)
# subscribe to it
irap_noroslib.Subscriber("/data", CustomData,
lambda m: print(m.id, m.label, m.where.x))#include "irap_noroslib.hpp"
using namespace irap_noroslib;
const std::string MSGS = "/home/me/robot_msgs"; // wherever you put them
const std::string PKG = "my_robot_msgs"; // the package they came from
MsgType Reading = load_msg_file(MSGS + "/Reading.msg", PKG);
MsgType CustomData = load_msg_file(MSGS + "/CustomData.msg", PKG);
SrvType GetStatus = load_srv_file(MSGS + "/GetStatus.srv", PKG);
set_master_uri("http://localhost:11311"); // Step 3
set_hostname("localhost");
init_node("my_node");
// publish it -- fields by name, nest with a dot, index with brackets
DynamicPublisher pub("/data", CustomData);
DynamicMessage m = CustomData.create();
m.set("id", 7).set("label", "hi");
m.set("header.frame_id", "base_link");
m.set_array("samples", std::vector<double>{1.0, 2.0});
m.set("where.x", 3.0);
m.append("readings").msg().set("value", 1.5).set("unit", "C");
pub.publish(m);
// subscribe to it
DynamicSubscriber sub("/data", CustomData, [](const DynamicMessage& m) {
loginfo(m.get<std::string>("label"));
double x = m.get<double>("where.x");
auto unit = m.get<std::string>("readings[0].unit"); // index into an array
});Note Reading.msg is loaded too, because CustomData nests it — every custom
type you use needs its own call, one per file. Order doesn't matter, and if you
forget one the error tells you exactly which file to add:
unknown message type "my_robot_msgs/Reading". It is nested by a type you loaded,
so load its file too:
load_msg_file("/path/to/Reading.msg", "my_robot_msgs")
Built-in types (std_msgs/Header, geometry_msgs/Point, …) need no call at all —
all 64 are already there.
ROS names a type pkg/Type, so a loose file can't tell you the pkg and you pass
it. (Only if the file still sits in a real catkin layout — <pkg>/msg/<Type>.msg —
is it inferred, and then you can omit it.)
It is not part of the md5sum, so a wrong package name does not by itself break the connection: ROS validates the md5, and a real subscriber will happily accept your data. That makes a typo here quietly harmful rather than loudly fatal. It matters for two things:
- Nested types by bare name.
Reading[] readingsinsidemy_robot_msgs/CustomDatameansmy_robot_msgs/Reading. Give the wrong package and it looks forwrong_pkg/Reading, which doesn't exist — the load fails outright. - Everything type-aware in ROS. The name you pass is the type real ROS sees.
Get it wrong and
rostopic echogivesERROR: Cannot load message class for [wrong_pkg/CustomData],rosbagrecords the wrong type, and anything checking the type string is misled — even though the bytes on the wire are perfectly correct.
The md5sum and the wire codec are derived from the file by the exact ROS
algorithm, so the type is precisely what rosmsg md5 computes and real ROS nodes
accept it. load_action_file works the same way and registers all 7 ROS action
types for you.
C++ also gets DynamicSubscriber, DynamicServiceServer, DynamicServiceClient,
DynamicActionClient and DynamicActionServer: the usual classes with the
compile-time type replaced by a loaded one.
from irap_noroslib import define_message
# md5 + wire codec derived automatically from the text
Pose2D = define_message("my_pkg/Pose2D", """
float64 x
float64 y
float64 theta
""")
pub = irap_noroslib.Publisher("/pose", Pose2D)
pub.publish(Pose2D(x=1.0, y=2.0, theta=3.14))Nest other registered types by full name (std_msgs/Header header,
my_pkg/Pose2D[] poses), use constants (uint8 FOO=1), fixed/variable arrays, and
every ROS builtin.
The typed, zero-cost option. Note the trade-off: you must supply the md5
yourself (rosmsg md5 <type>) — nothing derives it for you here. If you'd rather
not, use option 1.
#include "irap_noroslib.hpp"
struct Pose2D {
static constexpr const char* TYPE = "my_pkg/Pose2D";
static constexpr const char* MD5 = "938fa65709584ad8e77d238529be13b8"; // rosmsg md5
static constexpr const char* DEFINITION = "float64 x\nfloat64 y\nfloat64 theta\n";
double x = 0, y = 0, theta = 0;
std::vector<uint8_t> serialize() const {
irap_noroslib::Writer w; w.f64(x); w.f64(y); w.f64(theta); return w.b;
}
static Pose2D deserialize(const std::vector<uint8_t>& b) {
irap_noroslib::Reader r(b); Pose2D m; m.x = r.f64(); m.y = r.f64(); m.theta = r.f64(); return m;
}
};
// irap_noroslib::Publisher<Pose2D> pub("/pose"); / irap_noroslib::Subscriber<Pose2D> sub("/pose", cb);Getting the md5 wrong here matters: a publisher with a bad md5 is rejected by real ROS subscribers. (md5 discovery — below — rescues the subscriber side only: it learns the publisher's real md5 from the rejection and reconnects. It cannot fix a publisher.)
For a stamped message, make the first field a Header (Python: std_msgs/Header header; C++: compose std_msgs::Header in your codec) — see stamped_pub /
stamped_sub.
Verified end-to-end: a custom package built on real ROS with catkin_make, then
loaded from its bare files — in both languages all 11 md5s (3 messages, a
service, 7 action types) match rosmsg md5 / rossrv md5, and the types flow
both ways against genuine rospy nodes over topics, a service and an action.
Details in python/README.md / cpp/README.md.
Every ROS message type has an md5sum — a fingerprint of its fields. Publisher and subscriber must agree on it, or ROS refuses the connection:
[ERROR] Client [...] wants topic X to have datatype/md5sum [.../c3c0...],
but our version has [.../4193...]. Dropping connection.
NoROSLib reads the publisher's real md5 out of that error, adopts it, and reconnects — so you never get stuck on an md5 you got wrong when subscribing.
This rescues the subscriber side. It cannot fix a publisher: if you advertise
with a wrong md5, real ROS subscribers reject you and there is no error to learn
from. (NoROSLib plays the other role too, emitting the same rejection to peers that
present a wrong md5 to it.) If you don't want to think about md5s at all, load the
.msg file and let NoROSLib derive it — see
Step 9.
The same subcommands and the same arguments as the real tool, so your muscle memory
works. Available in both languages (nr_rostopic once pip-installed, or
python3 -m irap_noroslib.rostopic; ./cpp/build/nr_rostopic in C++).
nr_rostopic list [-v] # all topics (+ types, counts)
nr_rostopic type /chatter # std_msgs/String
nr_rostopic info /chatter # type, publishers, subscribers
nr_rostopic find std_msgs/String # topics of a type
nr_rostopic echo [-n N] [--noarr] /chatter # decode and print
nr_rostopic hz /chatter # publish rate
nr_rostopic bw /chatter # bandwidth
nr_rostopic pub [-r HZ | -1] /chatter std_msgs/String "data: hi"
# remember the master here, once (see below)
nr_rostopic --set_ros_master_uri http://HOST:11311 --set_ros_hostname YOUR_IPRather than repeating --master on every command, set it one time. nr_rostopic
writes what you give it to a master.yaml in the current directory and uses it
from then on:
nr_rostopic --set_ros_master_uri http://192.168.1.50:11311 \
--set_ros_hostname 192.168.1.77saved to ./master.yaml
ros_master_uri: http://192.168.1.50:11311
ros_hostname: 192.168.1.77
nr_rostopic in this directory now uses these -- no flags needed.
From now on, in that directory, just say what you want:
nr_rostopic list # no --master, no exports
nr_rostopic echo /chatterThe file is two plain lines, so you can also write it by hand or commit it with a project:
# master.yaml
ros_master_uri: http://192.168.1.50:11311
ros_hostname: 192.168.1.77ros_hostname is the how peers reach you value from
Step 3 — set it to your own IP when
the master is on a robot.
It deliberately beats the environment. A stale $ROS_MASTER_URI or $ROS_IP
left in your shell by some other ROS setup must not silently override what you
saved. Full precedence:
--master / --port / --host (this one command)
v
./master.yaml (what you saved here)
v
$ROS_MASTER_URI / $ROS_IP / $ROS_HOSTNAME
v
http://localhost:11311
Delete master.yaml to forget it. Since it lives in the current directory, each
project can have its own robot, and cd-ing elsewhere gets you a clean slate.
Or point it at a master for one command. --master takes a host, an IP, a
host:port, or a full URI, and --port sets the port — so these all mean the same
thing:
nr_rostopic --master 127.0.0.1 --port 11311 echo /chatter
nr_rostopic --master 127.0.0.1:11311 echo /chatter
nr_rostopic --master http://127.0.0.1:11311 echo /chatter
nr_rostopic --master 127.0.0.1 echo /chatter # port defaults to 11311If it cannot reach the master, it tells you which master and where that came from, which is usually the whole answer:
nr_rostopic: cannot reach the ROS master at http://this-does-not-exist:9999
(from $ROS_MASTER_URI)
[Errno -3] Temporary failure in name resolution
The C++ nr_rostopic takes the same flags and shares the same master.yaml —
either one can write the file and the other reads it.
Point it at a custom message type you have no .msg file for — one built on the
robot, in a catkin package you don't have — and it still decodes it:
$ rostopic echo /custom # the real tool, without the package built
ERROR: Cannot load message class for [noros_stress_msgs/CustomData]. Are your messages built?
$ nr_rostopic echo -n 1 /custom # same shell, no .msg file, no catkin package
header:
seq: 1
frame_id: "robot"
id: 42
samples: [1.5, 2.5]
label: "hi"
blob: [1, 2, 3]
location:
x: 9.5
valid: True
---Because a ROS publisher hands over the full message definition in the TCPROS
handshake — its own text plus every nested dependency — NoROSLib rebuilds the type
on the spot and decodes the bytes. Real rostopic echo refuses, because it needs
the message class built in a catkin package first.
The same trick is available in your own code: Subscriber(topic, None, cb) in
Python, AnySubscriber in C++ — subscribe to a topic whose type you have never
seen and get a decoded message.
NoROSLib can also be the roscore, not just talk to one. nr_roscore is a
standalone ROS master + parameter server (no ROS installed) that real ROS nodes
(rospy/roscpp), rostopic/rosservice/rosparam, and NoROSLib nodes all register
with. It implements the Master API (register/unregister publisher/subscriber/
service, lookupService/lookupNode, getSystemState, getPublishedTopics, …),
sends publisherUpdate to subscribers, and serves parameters.
After pip install irap_noroslib, nr_roscore is on your PATH:
nr_roscore # bind every interface, port 11311
nr_roscore --bind 127.0.0.1 --port 11311 # loopback only
nr_roscore --bind 0.0.0.0 --port 11322 # every interface, custom port
nr_roscore --host 192.168.1.10 # advertise this IP to nodes
./cpp/build/nr_roscore --port 11311 # C++ (built with the examples)--bind is the interface it listens on; --host is what it tells nodes to
connect back to. Give only --bind with a concrete IP and it is used for both —
so nr_roscore --bind 127.0.0.1 --port 11311 advertises http://127.0.0.1:11311/,
not the system hostname (a loopback-only master must not hand nodes a name that
resolves elsewhere).
Configuration (same knobs as a real roscore):
| Knob | Source (in precedence order) |
|---|---|
| port | --port › port in $ROS_MASTER_URI › 11311 |
| hostname | --host › a concrete --bind › $ROS_HOSTNAME › $ROS_IP › system hostname |
Then point any node at it: export ROS_MASTER_URI=http://<host>:<port>. Both
masters handle full (nested dict) parameter trees and auto-start a /rosout
→ /rosout_agg aggregator (disable with --no-rosout). Verified with real ROS:
two rostopic pub/echo nodes talk through nr_roscore,
rosservice/rosparam (incl. nested dicts)/rostopic list/info work against
it, and NoROSLib nodes use it as their master. Stress-tested: the 13-type topic
matrix (TCPROS+UDPROS), a 3,600-call service flood, concurrent parameter churn,
and a 5,400-op registration storm all pass against both masters.
Every example exists in both languages — same name, same behaviour. Run them
with a master up (Step 2) and ROS_MASTER_URI
set (Step 3).
python3 python/examples/<name>.py # Python: just run it
cmake -S cpp -B cpp/build # C++: build once...
cmake --build cpp/build -j
./cpp/build/<name> # ...then run| Example | Shows | Python | C++ |
|---|---|---|---|
| talker | publish std_msgs/String at 10 Hz |
✅ | ✅ |
| listener | subscribe std_msgs/String |
✅ | ✅ |
| custom_msg | your own message type — from text and from a .msg file |
✅ | ✅ |
| md5_discovery | subscribe with a wrong md5, auto-recover from the error | ✅ | ✅ |
| stamped_pub / stamped_sub | a custom message with a std_msgs/Header |
✅ | ✅ |
| add_two_ints_server / add_two_ints_client | a service (srv) server + client | ✅ | ✅ |
| fibonacci_server / fibonacci_client | an action (actionlib) server + client | ✅ | ✅ |
| params_example | parameters: get/set/has/delete (Python also search/list) | ✅ | ✅ |
| udp_listener | subscribe over UDPROS | ✅ | ✅ |
| nr_roscore | run your own ROS master | ✅ | ✅ |
| nr_rostopic | the rostopic CLI |
✅ | ✅ |
| webcam_pub | publish sensor_msgs/Image + CompressedImage from a webcam |
✅ | ✅ |
| webcam_sub | subscribe to those images and display them | ✅ | ✅ |
The two webcam examples require OpenCV (cv2 in Python,
<opencv2/opencv.hpp> in C++). OpenCV is not a dependency of NoROSLib — the
core library needs nothing beyond the standard library / a C++17 toolchain; only
these optional demos use it. The CMake build compiles them only if OpenCV is found.
(python/examples/ros_image_viewer.py is a real rospy node, kept as a
cross-check that the images we publish are decodable by genuine ROS.)
Cross-check any example with real ROS tools — rostopic echo/pub,
rosservice call, rosparam get/set, and the roscpp_tutorials /
rospy_tutorials / actionlib_tutorials packages.
nr_roscore --port 11311 & # 1) your own master
export ROS_MASTER_URI=http://localhost:11311 # 2) point at it
python3 python/examples/listener.py & # 3) receive
python3 python/examples/talker.py # 4) sendEvery built-in message type — and a custom message created on real ROS (a
catkin noros_stress_msgs/CustomData package) — round-trips through a real
roscore in both languages, decoded by genuine rospy subscribers (so the
md5 handshake + wire bytes are checked by ROS itself):
| types published | verified by real ROS | |
|---|---|---|
| Python | 64 built-ins + custom | 65 / 65 ✅ |
| C++ | 64 built-ins + custom | 65 / 65 ✅ |
The custom message's md5 that NoROSLib computes (90f5077…) is identical to the
one real ROS's rosmsg md5 generates for the same .msg, and it flows both ways
(NoROSLib → rostopic echo, and a real rospy publisher → NoROSLib subscriber) in
Python and C++.
- Register with the master (
registerPublisher/registerSubscriber) and stand up a slave XML-RPC server so the master and peers can callrequestTopic/publisherUpdate. - Publisher: open a TCPROS (and UDPROS) socket; on each subscriber, read its
connection header, check the md5, send ours, then stream
[len][body]. - Subscriber: for each publisher the master reports,
requestTopic→ connect → exchange headers. On an md5 error, learn the real md5 and reconnect; otherwise adopt the publisher's type/md5 and deliver decoded messages to your callback. - Services / actions / parameters ride the same master + TCPROS machinery.
Message bodies are the exact bytes ROS puts on the wire, so the codecs are
byte-for-byte compatible with real ROS (every md5sum matches rosmsg/rossrv).
python/ pip-installable package (irap_noroslib/), examples/, tests/
cpp/ single header (include/irap_noroslib.hpp), examples/, dev/ (source of truth)
LICENSE Non-Commercial License
Created by iRAP Robot, King Mongkut's University of Technology North Bangkok (KMUTNB).
Non-Commercial License — see LICENSE.
