ROS2 - Helping the community [part2]
So, where to begin?
I tried to use this ROS2 tello_driver. Back then it seemed promising, but after some messing around, I've learned that this ROS2 tello_driver is intended for tello EDU, which uses SKD 2.0, while the regular tello uses SDK 1.3.
DJI tend to do so in their so-called open-API products, like Tello and RobomasterS1 (we have one of these in work, for fun and hacking purposes).
tl;dr
In the previous post, I realized that the ROS2 tello_driver's flight_data message was missing the POS data, which is relevant for making an Autonomous drone. Hence I hacked the sh**t out of the good old tello_driver (I worked with it before), and of this ROS2 tello_driver as well, and trying to figure out what makes them tick.
As part of learning and improving ROS2 & C++ skills, in this post, I will share the journey I went through, and what I've learned on the way.
Let's dive in!
The next step was figuring out which communication protocol does the ROS2 tello_driver uses, and what are the differences between this protocol and the one that TelloPy's driver uses. According to their explanation, the ROS2 tello_driver is implemented around the DJI's SDK. DJI released two types of SDK's: 1.3, and 2.0, therefore I needed to get to know them both, here is what I learned:
SDK 2.0
According to DJI sdk 2 documentation:
The user can send many HIGH-LEVEL commands through port: 8889, such as:
- takeoff
- land
- flip
- go x y z speed mid <--- 'mid' == Mission ID
- Many more high-level commands..
The Tello sends back its state (through port: 8990):
- mid
- x
- y
- z
- pith
- Many more parameters..
- Well, that's lovely.
If the drone knows it's position (Only god knows how it is being inferred - for me it is a BlackBox), then the goal of building an autonomous drone is more feasible then before!
In the previous post, showed that installing ROS2 tello_driver is easy as plug and play, also I posted a short video showing the /flight_data
topic ( a ROS convention for message):
One can see the empty x y z fields of the message.
After some digging in the ROS2 tello_driver code, I found that it has a state_socket thread, which performs minimal parsing from the raw data:
// Process a state packet from the drone runs at 10Hz
void StateSocket::process_packet(size_t r)
{
std::lock_guard<std::mutex> lock(mtx_);
static std::map<uint8_t, std::string> enum_names{
{tello_msgs::msg::FlightData::SDK_UNKNOWN, "unknown"},
{tello_msgs::msg::FlightData::SDK_1_3, "v1.3"},
{tello_msgs::msg::FlightData::SDK_2_0, "v2.0"}};
receive_time_ = driver_->now();
if (receiving_ && driver_->count_subscribers(driver_->flight_data_pub_->get_topic_name()) == 0) {
// Nothing to do
return;
}
// Split on ";" and ":" and generate a key:value map
std::map<std::string, std::string> fields;
std::string raw(buffer_.begin(), buffer_.begin() + r);
std::regex re("([^:]+):([^;]+);");
for (auto i = std::sregex_iterator(raw.begin(), raw.end(), re); i != std::sregex_iterator(); ++i) {
auto match = *i;
fields[match[1]] = match[2];
}
// First message?
if (!receiving_) {
receiving_ = true;
// Hack to figure out the SDK version
sdk_ = tello_msgs::msg::FlightData::SDK_1_3;
auto i = fields.find("mid");
if (i != fields.end() && i->second != "257") {
sdk_ = tello_msgs::msg::FlightData::SDK_2_0;
}
RCLCPP_INFO(driver_->get_logger(), "Receiving state, SDK version %s", enum_names[sdk_].c_str());
}
// Only send ROS messages if there are subscribers
if (driver_->count_subscribers(driver_->flight_data_pub_->get_topic_name()) > 0) {
tello_msgs::msg::FlightData msg;
msg.header.stamp = receive_time_;
msg.raw = raw;
msg.sdk = sdk_;
try {
if (sdk_ == tello_msgs::msg::FlightData::SDK_2_0) {
msg.mid = std::stoi(fields["mid"]);
msg.x = std::stoi(fields["x"]);
msg.y = std::stoi(fields["y"]);
msg.z = std::stoi(fields["z"]);
}
msg.pitch = std::stoi(fields["pitch"]);
msg.roll = std::stoi(fields["roll"]);
msg.yaw = std::stoi(fields["yaw"]);
msg.vgx = std::stoi(fields["vgx"]);
msg.vgy = std::stoi(fields["vgy"]);
msg.vgz = std::stoi(fields["vgz"]);
msg.templ = std::stoi(fields["templ"]);
msg.temph = std::stoi(fields["temph"]);
msg.tof = std::stoi(fields["tof"]);
msg.h = std::stoi(fields["h"]);
msg.bat = std::stoi(fields["bat"]);
msg.baro = std::stof(fields["baro"]);
msg.time = std::stoi(fields["time"]);
msg.agx = std::stof(fields["agx"]);
msg.agy = std::stof(fields["agy"]);
msg.agz = std::stof(fields["agz"]);
} catch (std::exception e) {
RCLCPP_ERROR(driver_->get_logger(), "Can't parse flight data");
return;
}
driver_->flight_data_pub_->publish(msg);
}
}
Let's break it down:
It seems the SDK is being inferred from the message:
// Hack to figure out the SDK version
sdk_ = tello_msgs::msg::FlightData::SDK_1_3;
auto i = fields.find("mid");
if (i != fields.end() && i->second != "257") {
sdk_ = tello_msgs::msg::FlightData::SDK_2_0;
}
Accordingly, x y z fields are being filled:
if (sdk_ == tello_msgs::msg::FlightData::SDK_2_0) {
msg.mid = std::stoi(fields["mid"]);
msg.x = std::stoi(fields["x"]);
msg.y = std::stoi(fields["y"]);
msg.z = std::stoi(fields["z"]);
}
According to the SDK 2.0; drone state
is being published to port 8890.
Using Wireshark, I saw that the packet sent to port 8890 contains parsed data. That fits the sdk 2.0 description:
Packet's data
section contains drone state in one line:
pitch:0;roll:0;yaw:-5;vgx:0;vgy:0;vgz:0;templ:60;temph:62;tof:10;h:0;bat:63;baro:149.54;time:0;agx:-15.00;agy:1.00;agz:-998.00;
This oneliner can be easily parsed using this parser:
std::regex re("([^:]+):([^;]+);");
And with this parser, data is being inserted from the message into fields
:
for (auto i = std::sregex_iterator(raw.begin(), raw.end(), re); i != std::sregex_iterator(); ++i) {
auto match = *i;
fields[match[1]] = match[2];
}
The final ROS2 message look likes that:
(using the following command, in a new terminal: ros2 topic echo /flight_data
)
header:
stamp:
sec: 1601124796
nanosec: 806872198
frame_id: ''
raw: "pitch:0;roll:0;yaw:-5;vgx:0;vgy:0;vgz:0;templ:60;temph:62;tof:10;h:0;bat:63;baro:149.54;time:0;agx:-15.00;agy:1.00;agz:-998.00;\r..."
sdk: 1 #<---------------------------- MY DRONE's SDK (1.3)
pitch: 0.0
roll: 0.0
yaw: -5.0
vgx: 0
vgy: 0
vgz: 0
templ: 60
temph: 62
tof: 10
h: 0
bat: 63
baro: 149.5399932861328
time: 0
agx: -15.0
agy: 1.0
agz: -998.0
mid: 0 #<---------------------------- EMPTY - MY DRONE IS USING SDK 1.3
x: 0.0 #<---------------------------- EMPTY - MY DRONE IS USING SDK 1.3
y: 0.0 #<---------------------------- EMPTY - MY DRONE IS USING SDK 1.3
z: 0.0 #<---------------------------- EMPTY - MY DRONE IS USING SDK 1.3
---
Using the good-old TelloPy driver
Surprisingly, after running the TelloPy's 'simpletakeoff.py' script and subscribing to 'drone.EVENTLOG_DATA', data was successfully received from the drone, including POS!!!
This is the simple_takeoff.py
script I ran (with minor modifications):
from time import sleep
import sys
sys.path.append("/home/gal/sandbox/tello_py/TelloPy") # Simple hack I did to resolve PATH issues.
import tellopy
def handler(event, sender, data, **args):
drone = sender
if event is drone.EVENT_LOG_DATA:
print(data) # <------------------------------ THE FOLLOWING MESSAGE
def test():
drone = tellopy.Tello()
try:
drone.subscribe(drone.EVENT_LOG_DATA, handler)
drone.connect()
drone.wait_for_connection(60.0)
while(1):
inp = input()
# drone.takeoff()
# sleep(5)
# drone.down(50)
# sleep(5)
# drone.land()
# sleep(5)
except Exception as ex:
print(ex)
finally:
drone.quit()
if __name__ == '__main__':
test()
And this is how the drone.EVENT_LOG_DATA
looks like:
MVO: VEL: 0.00 0.00 0.00 POS: 0.02 0.01 -0.01|IMU: ACC: -0.02 -0.00 -1.01 GYRO: 0.00 0.00 -0.00 QUATERNION: 1.00 -0.00 -0.00 0.00 VG: -0.27 -0.15 -0.06
Can you see the POS: 0.02 0.01 -0.01
?!
What is happening under the hood?
Well, it seems that hanyazou wrapped a go_lang project that hacked the Tello packets protocol.
Looking again at the simple_takeoff.py
:
drone = tellopy.Tello() # First a Tello object is instantiated
Tello.py
module looks like that:
class Tello(object):
EVENT_CONNECTED = event.Event('connected')
EVENT_WIFI = event.Event('wifi')
EVENT_LIGHT = event.Event('light')
# Bunch of CONSTANTS
*
*
*
*
# Bunch of CONSTANTS
def __init__(self, port=9000):
self.tello_addr = ('192.168.10.1', 8889)
self.debug = False
self.pkt_seq_num = 0x01e4
self.port = port
self.udpsize = 2000
# Bunch of initialisers
*
*
*
# Bunch of initializers
# Create a UDP socket
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind(('', self.port)) #<----------------------------- BIND THAT SOCKET TO THE GIVEN PORT
self.sock.settimeout(2.0)
dispatcher.connect(self.__state_machine, dispatcher.signal.All)
threading.Thread(target=self.__recv_thread).start() # <-------- THREAD FOR RECEIVING DATA
threading.Thread(target=self.__video_thread).start()
Wait what?! what is port 9000????
It appears that there's an undocumented port, through which all the drone's data is 'spilled' into.
After sniffing that port with Wireshark:
There's no human-readable
data here.
And yet, the driver extrapolated data successfully.
So I looked even deeper...
This is what the __recv_thread looks like:
def __recv_thread(self):
sock = self.sock
while self.state != self.STATE_QUIT:
if self.state == self.STATE_CONNECTED:
self.__send_stick_command() # ignore errors <---- *KEEP ALIVE
try:
data, server = sock.recvfrom(self.udpsize)
log.debug("recv: %s" % byte_to_hexstring(data))
self.__process_packet(data) # <------------------ THIS IS WHAT I'M INTRESTED IN
except socket.timeout as ex:
if self.state == self.STATE_CONNECTED:
log.error('recv: timeout')
self.__publish(event=self.__EVENT_TIMEOUT)
except Exception as ex:
log.error('recv: %s' % str(ex))
show_exception(ex)
Side Note
*KEEP ALIVE
- The Tello drone turns itself OFF if no command is received within 30 sec. Hence for every data the Tello drone sends to the TelloPy client, self.__send_stick_command()
is called to simulate stick commands back to the Tello drone.
Before diving into self.__process_packet(data)
, Some preperation is needed.
Inside protocol.py
, hanyazou mapped all possible commands to their HEX value:
# low-level Protocol (https://tellopilots.com/wiki/protocol/#MessageIDs)
START_OF_PACKET = 0xcc
SSID_MSG = 0x0011
SSID_CMD = 0x0012
SSID_PASSWORD_MSG = 0x0013
SSID_PASSWORD_CMD = 0x0014
WIFI_REGION_MSG = 0x0015
WIFI_REGION_CMD = 0x0016
WIFI_MSG = 0x001a
VIDEO_ENCODER_RATE_CMD = 0x0020
VIDEO_DYN_ADJ_RATE_CMD = 0x0021
EIS_CMD = 0x0024
VIDEO_START_CMD = 0x0025
VIDEO_RATE_QUERY = 0x0028
TAKE_PICTURE_COMMAND = 0x0030
VIDEO_MODE_CMD = 0x0031
VIDEO_RECORD_CMD = 0x0032
EXPOSURE_CMD = 0x0034
LIGHT_MSG = 0x0035
JPEG_QUALITY_MSG = 0x0037
ERROR_1_MSG = 0x0043
ERROR_2_MSG = 0x0044
VERSION_MSG = 0x0045
TIME_CMD = 0x0046
ACTIVATION_TIME_MSG = 0x0047
LOADER_VERSION_MSG = 0x0049
STICK_CMD = 0x0050
TAKEOFF_CMD = 0x0054
LAND_CMD = 0x0055
FLIGHT_MSG = 0x0056
SET_ALT_LIMIT_CMD = 0x0058
FLIP_CMD = 0x005c
THROW_AND_GO_CMD = 0x005d
PALM_LAND_CMD = 0x005e
TELLO_CMD_FILE_SIZE = 0x0062 # pt50
TELLO_CMD_FILE_DATA = 0x0063 # pt50
TELLO_CMD_FILE_COMPLETE = 0x0064 # pt48
SMART_VIDEO_CMD = 0x0080
SMART_VIDEO_STATUS_MSG = 0x0081
LOG_HEADER_MSG = 0x1050
LOG_DATA_MSG = 0x1051
LOG_CONFIG_MSG = 0x1052
BOUNCE_CMD = 0x1053
CALIBRATE_CMD = 0x1054
LOW_BAT_THRESHOLD_CMD = 0x1055
ALT_LIMIT_MSG = 0x1056
LOW_BAT_THRESHOLD_MSG = 0x1057
ATT_LIMIT_CMD = 0x1058 # Stated incorrectly by Wiki (checked from raw packets)
ATT_LIMIT_MSG = 0x1059
EMERGENCY_CMD = 'emergency'
Also, I've learned from the go_lang project the structure of the Tello drone packets:
Position | Usage |
---|---|
0 | 0xcc indicates the beggining of a packet |
1-2 | Packet size, 13 bit encoded ([2] << 8) | ([1] >> 3 ) |
3 | CRC8 of bytes 0-2 |
4 | Packet type ID |
5-6 | Command ID, little endian |
7-8 | Sequence number of the packet, little endian |
9-(n-2) | Data (if any) |
(n-1)-n | CRC16 of bytes 0 to n-2 |
So for example: 0xcc 0x58 0x00 0x7c 0x68 0x54 0x00 0xe4 0x01 0xc2 0x16
Value | Usage |
---|---|
0xcc | the beggining of a packet |
0x58 0x00 | Packet size of 11 |
0x7c | CRC8 of bytes 0-2 |
0x68 | Packet type ID |
0x54 0x00 | "Takeoff" command ID, little endian |
0xe4 0x01 | Sequence number of the packet, little-endian |
0xc2 0x16 | CRC16 of bytes 0 to 8 |
Knowing the packet structure, and using the messages mapping described above, I can now understand the sniffed package from port 9000:
0xcc 0xa0 0x11 0x50 0x88 0x51 0x10 0x14 0x02 0x00 0x55 0x43 0x00 0x15 .....
Value | Usage |
---|---|
0xcc | the beggining of a packet |
0xa0 0x11 | Packet size of 564.0 |
0x50 | CRC8 of bytes 0-2 |
0x88 | Packet type ID |
0x51 0x10 | "LOG_DATA_MSG" command ID, little endian, hence: 0x1051 |
0x14 0x02 | Sequence number of the packet, little-endian |
0x00 0x55 0x43 ..... | Data |
As for processing a packet:
def __process_packet(self, data):
if isinstance(data, str):
data = bytearray([x for x in data])
if str(data[0:9]) == 'conn_ack:' or data[0:9] == b'conn_ack:':
log.info('connected. (port=%2x%2x)' % (data[9], data[10]))
log.debug(' %s' % byte_to_hexstring(data))
if self.video_enabled:
self.__send_exposure()
self.__send_video_encoder_rate()
self.__send_start_video()
self.__publish(self.__EVENT_CONN_ACK, data)
return True
if data[0] != START_OF_PACKET:
log.info('start of packet != %02x (%02x) (ignored)' % (START_OF_PACKET, data[0]))
log.info(' %s' % byte_to_hexstring(data))
log.info(' %s' % str(map(chr, data))[1:-1])
return False
pkt = Packet(data)
cmd = uint16(data[5], data[6])
if cmd == LOG_HEADER_MSG:
id = uint16(data[9], data[10])
log.info("recv: log_header: id=%04x, '%s'" % (id, str(data[28:54])))
log.debug("recv: log_header: %s" % byte_to_hexstring(data[9:]))
self.__send_ack_log(id)
self.__publish(event=self.EVENT_LOG_HEADER, data=data[9:])
if self.log_data_file and not self.log_data_header_recorded:
self.log_data_file.write(data[12:-2])
self.log_data_header_recorded = True
elif cmd == LOG_DATA_MSG:
log.debug("recv: log_data: length=%d, %s" % (len(data[9:]), byte_to_hexstring(data[9:])))
self.__publish(event=self.EVENT_LOG_RAWDATA, data=data[9:])
try:
self.log_data.update(data[10:])
if self.log_data_file:
self.log_data_file.write(data[10:-2])
except Exception as ex:
log.error('%s' % str(ex))
self.__publish(event=self.EVENT_LOG_DATA, data=self.log_data)
elif cmd == LOG_CONFIG_MSG:
log.debug("recv: log_config: length=%d, %s" % (len(data[9:]), byte_to_hexstring(data[9:])))
self.__publish(event=self.EVENT_LOG_CONFIG, data=data[9:])
elif cmd == WIFI_MSG:
log.debug("recv: wifi: %s" % byte_to_hexstring(data[9:]))
self.wifi_strength = data[9]
self.__publish(event=self.EVENT_WIFI, data=data[9:])
elif cmd == ALT_LIMIT_MSG:
log.info("recv: altitude limit: %s" % byte_to_hexstring(data[9:-2]))
elif cmd == ATT_LIMIT_MSG:
log.info("recv: attitude limit: %s" % byte_to_hexstring(data[9:-2]))
elif cmd == LOW_BAT_THRESHOLD_MSG:
log.info("recv: low battery threshold: %s" % byte_to_hexstring(data[9:-2]))
elif cmd == LIGHT_MSG:
log.debug("recv: light: %s" % byte_to_hexstring(data[9:-2]))
self.__publish(event=self.EVENT_LIGHT, data=data[9:])
elif cmd == FLIGHT_MSG:
flight_data = FlightData(data[9:])
flight_data.wifi_strength = self.wifi_strength
log.debug("recv: flight data: %s" % str(flight_data))
self.__publish(event=self.EVENT_FLIGHT_DATA, data=flight_data)
elif cmd == TIME_CMD:
log.debug("recv: time data: %s" % byte_to_hexstring(data))
self.__publish(event=self.EVENT_TIME, data=data[7:9])
elif cmd in (SET_ALT_LIMIT_CMD, ATT_LIMIT_CMD, LOW_BAT_THRESHOLD_CMD, TAKEOFF_CMD, LAND_CMD, VIDEO_START_CMD, VIDEO_ENCODER_RATE_CMD, PALM_LAND_CMD,
EXPOSURE_CMD, THROW_AND_GO_CMD, EMERGENCY_CMD):
log.debug("recv: ack: cmd=0x%02x seq=0x%04x %s" %
(uint16(data[5], data[6]), uint16(data[7], data[8]), byte_to_hexstring(data)))
elif cmd == TELLO_CMD_FILE_SIZE:
# Drone is about to send us a file. Get ready.
# N.b. one of the fields in the packet is a file ID; by demuxing
# based on file ID we can receive multiple files at once. This
# code doesn't support that yet, though, so don't take one photo
# while another is still being received.
log.info("recv: file size: %s" % byte_to_hexstring(data))
if len(pkt.get_data()) >= 7:
(size, filenum) = struct.unpack('<xLH', pkt.get_data())
log.info(' file size: num=%d bytes=%d' % (filenum, size))
# Initialize file download state.
self.file_recv[filenum] = DownloadedFile(filenum, size)
else:
# We always seem to get two files, one with most of the payload missing.
# Not sure what the second one is for.
log.warn(' file size: payload too small: %s' % byte_to_hexstring(pkt.get_data()))
# Ack the packet.
self.send_packet(pkt)
elif cmd == TELLO_CMD_FILE_DATA:
# log.info("recv: file data: %s" % byte_to_hexstring(data[9:21]))
# Drone is sending us a fragment of a file it told us to prepare
# for earlier.
self.recv_file_data(pkt.get_data())
else:
log.info('unknown packet: %04x %s' % (cmd, byte_to_hexstring(data)))
return False
return True
While Breaking down this packet processor. Although it seemed scary with all that long code, it all went down to this line::
# Extract the command type from the packet and process it accordingly.
cmd = uint16(data[5], data[6])
Earlier, in simplte_takeoff.py
, I subscribed to drone.subscribe(drone.EVENT_LOG_DATA, handler)
. This is the type of packet I looked for in this packet_processor:
elif cmd == LOG_DATA_MSG:
log.debug("recv: log_data: length=%d, %s" % (len(data[9:]), byte_to_hexstring(data[9:])))
self.__publish(event=self.EVENT_LOG_RAWDATA, data=data[9:])
try:
self.log_data.update(data[10:])
if self.log_data_file:
self.log_data_file.write(data[10:-2])
except Exception as ex:
log.error('%s' % str(ex))
self.__publish(event=self.EVENT_LOG_DATA, data=self.log_data) # <------------- MY SUBSCRIBER BEING NOTIFIED
This line is the actual data extraction. This is what I looked for:
self.log_data.update(data[10:])
Bare with me, Its almost over :)
Peeking inside log_data.update()
:
def update(self, data):
if isinstance(data, bytearray):
data = str(data)
self.log.debug('LogData: data length=%d' % len(data))
self.count += 1
pos = 0
while (pos < len(data) - 2):
if (struct.unpack_from('B', data, pos+0)[0] != 0x55):
raise Exception('LogData: corrupted data at pos=%d, data=%s'
% (pos, byte_to_hexstring(data[pos:])))
length = struct.unpack_from('<h', data, pos+1)[0]
checksum = data[pos+3]
id = struct.unpack_from('<H', data, pos+4)[0]
# 4bytes data[6:9] is tick
# last 2 bytes are CRC
# length-12 is the byte length of payload
xorval = data[pos+6]
if isinstance(data, str):
payload = bytearray([ord(x) ^ ord(xorval) for x in data[pos+10:pos+10+length-12]])
else:
payload = bytearray([x ^ xorval for x in data[pos+10:pos+10+length-12]])
if id == self.ID_NEW_MVO_FEEDBACK:
self.mvo.update(payload, self.count) # <--------- THIS IS WHERE 'POS' DATA CAN BE FOUND
elif id == self.ID_IMU_ATTI:
self.imu.update(payload, self.count)
else:
if not id in self.unknowns:
self.log.info('LogData: UNHANDLED LOG DATA: id=%5d, length=%4d' % (id, length-12))
self.unknowns.append(id)
pos += length
if pos != len(data) - 2:
raise Exception('LogData: corrupted data at pos=%d, data=%s'
% (pos, byte_to_hexstring(data[pos:])))
Jackpot!
This line:
payload = bytearray([x ^ xorval for x in data[pos+10:pos+10+length-12]])
And this one:
self.mvo.update(payload, self.count)
Now that I've found a way to extract POS from the packet that comes from port 9000, I need to implement a similar packet_processor in CPP, and add it to the ROS2 tello_driver.
So, what is the difference?
- SUPPORTs ROS2 (discussed in the previous post).
- Uses a documented SDK, supplied by DJI.
- Listens to port
8890
, which according to the SDK, suppliesdrone state
:human-readable
data, that can be easily parsed and published.
It has known serialized structure:pitch:0;roll:0;yaw:-5;vgx:0;vgy:0;vgz:0...
- CPP
- Fast.
- Relevant for me.
TelloPy driver:
- NO ROS2 SUPPORT
- Listens to port
9000
- An undocumented open port at the drone, this port appears to sendRAW_LOG_DATA
that contains EVERYTHING! It has been discovered by the community - with reverse engineering (and probably lots of hours using Wireshark) - PYTHON only
- Easy to understand & maintain & hack.
Final thoughts
In this post, I've focused on the journey I took.
It seems that I'm ought to implement a CPP packet_processor, for the ROS2 tello_driver. Thus adding support for RAW_LOG_DATA
logging capabilities.
This is an excellent opportunity to support the development of ROS2. This mini-series is all about supporting the ROS2 community. I do have thoughts about POS
data accuracy/availability though, will it be enough for a position estimator? is it solid enough to be considered in the VSLAM
algorithm?
While the questions arise, the quest must go on.
Cheers, Gal.