add queue service for tasks
This commit is contained in:
parent
70dca46f26
commit
b4d4b8133d
5 changed files with 1172 additions and 1 deletions
|
@ -1,6 +1,6 @@
|
||||||
obj_id,device_type,device_name,supply,freq,napor,mass,power,temp_range,date,serial_num,ta_range,pump_num,i_ob,i_sum,ip,engine_num
|
obj_id,device_type,device_name,supply,freq,napor,mass,power,temp_range,date,serial_num,ta_range,pump_num,i_ob,i_sum,ip,engine_num
|
||||||
Object ID,Тип устройства,Наименование,"Q - подача, м³/ч",n - частота оборотов,"H - напор, м",m - масса,P2 - мощность,T - диапазон температур,Дата производства,Заводской номер,Диапазон ta в градусах цельсия,Кол-во насосов,Iоб,Iсум,IP,Кол-во электродвигателей
|
Object ID,Тип устройства,Наименование,"Q - подача, м³/ч",n - частота оборотов,"H - напор, м",m - масса,P2 - мощность,T - диапазон температур,Дата производства,Заводской номер,Диапазон ta в градусах цельсия,Кол-во насосов,Iоб,Iсум,IP,Кол-во электродвигателей
|
||||||
1,Насос общепромышленный,,50,1450 об/мин,30,200.0 кг,15.0 кВт,-10...+80°C,01-15-23,SN12345,,,,,,
|
1,Насос общепромышленный,"КММ-ХА 80-50-200б/2/18,5-Е-55Т/BBQV-HC-У3",50,1450 об/мин,30,200.0 кг,15.0 кВт,-10...+80°C,01-15-23,SN12345,,,,,,
|
||||||
2,Насос взрывозащищённый (Ex),"КММ-ХА 80-50-200б/2/18,5-Е-55Т/BBQV-HC-У3",30 м³/ч,1450 об/мин,25,180.0 кг,10.0 кВт,-20...+60°C,02-10-23,SN12346,-40...+85°C,,,,,
|
2,Насос взрывозащищённый (Ex),"КММ-ХА 80-50-200б/2/18,5-Е-55Т/BBQV-HC-У3",30 м³/ч,1450 об/мин,25,180.0 кг,10.0 кВт,-20...+60°C,02-10-23,SN12346,-40...+85°C,,,,,
|
||||||
3,Насосная установка,,100 м³/ч,,50,500.0 кг,30.0 кВт,,03-01-23,SN12347,2,,55,4,
|
3,Насосная установка,,100 м³/ч,,50,500.0 кг,30.0 кВт,,03-01-23,SN12347,2,,55,4,
|
||||||
4,Шкаф управления,,,,,,,,04-05-23,SN12348,,,30.0A,50.0A,IP54,3
|
4,Шкаф управления,,,,,,,,04-05-23,SN12348,,,30.0A,50.0A,IP54,3
|
||||||
|
|
Can't render this file because it has a wrong number of fields in line 5.
|
38
rbs_mill_assist/config/obj_cfg.json
Normal file
38
rbs_mill_assist/config/obj_cfg.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"obj_id": 1,
|
||||||
|
"place_name": "bunker_1",
|
||||||
|
"place_aruco": 21,
|
||||||
|
"place_pos": "{pose}",
|
||||||
|
"graver_template": "template_obj_1.svg",
|
||||||
|
"dimensions": "{X,Y,Z}",
|
||||||
|
"skills": ["sid", "..."]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obj_id": 2,
|
||||||
|
"place_name": "bunker_2",
|
||||||
|
"place_aruco": 22,
|
||||||
|
"place_pos": "{pose}",
|
||||||
|
"graver_template": "{file}",
|
||||||
|
"dimensions": "{X,Y,Z}",
|
||||||
|
"skills": ["sid", "..."]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obj_id": 3,
|
||||||
|
"place_name": "bunker_3",
|
||||||
|
"place_aruco": 23,
|
||||||
|
"place_pos": "{pose}",
|
||||||
|
"graver_template": "{file}",
|
||||||
|
"dimensions": "{X,Y,Z}",
|
||||||
|
"skills": ["sid", "..."]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obj_id": 4,
|
||||||
|
"place_name": "bunker_4",
|
||||||
|
"place_aruco": 24,
|
||||||
|
"place_pos": "{pose}",
|
||||||
|
"graver_template": "{file}",
|
||||||
|
"dimensions": "{X,Y,Z}",
|
||||||
|
"skills": ["sid", "..."]
|
||||||
|
}
|
||||||
|
]
|
855
rbs_mill_assist/config/template_obj_1.svg
Normal file
855
rbs_mill_assist/config/template_obj_1.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 177 KiB |
|
@ -1,5 +1,6 @@
|
||||||
install(PROGRAMS
|
install(PROGRAMS
|
||||||
grasping_service.py
|
grasping_service.py
|
||||||
get_key_pose_frame.py
|
get_key_pose_frame.py
|
||||||
|
queue_srv.py
|
||||||
DESTINATION lib/${PROJECT_NAME}
|
DESTINATION lib/${PROJECT_NAME}
|
||||||
)
|
)
|
||||||
|
|
277
rbs_mill_assist/scripts/queue_srv.py
Normal file
277
rbs_mill_assist/scripts/queue_srv.py
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
|
import rclpy
|
||||||
|
import json
|
||||||
|
from ament_index_python.packages import get_package_share_directory
|
||||||
|
from rbs_utils_interfaces.srv import AddTasks, TaskFromQueue
|
||||||
|
from rclpy.node import Node
|
||||||
|
from rclpy.service import Service
|
||||||
|
|
||||||
|
import cairosvg
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
FILE_QUEUE = "tasks_queue.json"
|
||||||
|
FILE_QCFG = "obj_cfg.json" # obj_id должны идти последовательно, начиная с 1
|
||||||
|
FILE_TEMP_LIST = "temp.txt"
|
||||||
|
|
||||||
|
KEY_QUEUE = "Queue"
|
||||||
|
KEY_TASK_INDEX = "TaskId"
|
||||||
|
KEY_OBJECT_INDEX = "obj_id"
|
||||||
|
KEY_GRAVER_TMPL = "graver_template"
|
||||||
|
KEY_GRAVER_PNG = "graver_png"
|
||||||
|
KEY_GRAVER_SVG = "graver_svg"
|
||||||
|
KEY_CSV_TASK = "csv"
|
||||||
|
|
||||||
|
XMLTAG_FOR_REPLACE = "text"
|
||||||
|
|
||||||
|
FACTOR_mm_to_pixel = 10 # mm to pixel conversion factor
|
||||||
|
|
||||||
|
def get_tags_without_namespace(element):
|
||||||
|
"""
|
||||||
|
Получаем имя тега без пространства имен
|
||||||
|
"""
|
||||||
|
return element.tag.split('}')[-1] if '}' in element.tag else element.tag
|
||||||
|
|
||||||
|
class QueueService(Node):
|
||||||
|
__TASK_NO = 0
|
||||||
|
__TASK_TAKE_ON = 1
|
||||||
|
__TASK_COMPLETED = 2
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("queue_service")
|
||||||
|
self._cstate = self.__TASK_NO
|
||||||
|
self.sz_queue = 0 # size of queue
|
||||||
|
self.task_last_index = 0
|
||||||
|
self.TaskDict: Dict = {} # dictionary with parameters of the current task
|
||||||
|
self.cfgdir = os.path.join(get_package_share_directory("rbs_mill_assist"), "config")
|
||||||
|
self.workdir = os.path.join(self.cfgdir, "queue")
|
||||||
|
self.fn_queue = os.path.join(self.workdir, FILE_QUEUE) # file with data of queue
|
||||||
|
self.fn_cfg = os.path.join(self.cfgdir, FILE_QCFG) # file with data of objects config
|
||||||
|
self.fn_tmp = os.path.join(self.workdir, FILE_TEMP_LIST) # file with list of temp files
|
||||||
|
|
||||||
|
os.makedirs(self.workdir, exist_ok=True)
|
||||||
|
if os.path.isfile(self.fn_queue):
|
||||||
|
self.init_queue()
|
||||||
|
else:
|
||||||
|
with open(self.fn_queue, "w") as fh:
|
||||||
|
json.dump({KEY_QUEUE:[]}, fh)
|
||||||
|
|
||||||
|
self.srv_add: Service = self.create_service(AddTasks, "queue/add_tasks", self.add_tasks)
|
||||||
|
self.srv_get: Service = self.create_service(AddTasks, "queue/get_task", self.get_task)
|
||||||
|
self.srv_takeon: Service = self.create_service(TaskFromQueue, "queue/takeon_task", self.takeon_task)
|
||||||
|
self.srv_completed: Service = self.create_service(TaskFromQueue, "queue/task_completed", self.task_completed)
|
||||||
|
self.get_logger().info(f"'queue_service' loaded")
|
||||||
|
|
||||||
|
def get_task(self, request: TaskFromQueue.Request, response: TaskFromQueue.Response) -> TaskFromQueue.Response:
|
||||||
|
"""
|
||||||
|
Get json-string with parameters of the current task
|
||||||
|
"""
|
||||||
|
response.ok = False
|
||||||
|
if self._cstate == self.__TASK_TAKE_ON: # есть текущее задание
|
||||||
|
response.task = json.dumps(self.TaskDict, ensure_ascii=False)
|
||||||
|
response.ok = True
|
||||||
|
return response
|
||||||
|
|
||||||
|
def task_completed(self, request: TaskFromQueue.Request, response: TaskFromQueue.Response) -> TaskFromQueue.Response:
|
||||||
|
"""
|
||||||
|
Сomplete the current task
|
||||||
|
"""
|
||||||
|
response.ok = False
|
||||||
|
if self._cstate == self.__TASK_TAKE_ON: # есть текущее задание
|
||||||
|
# добавим файлы для удаления, которые больше не нужны
|
||||||
|
with open(self.fn_tmp, "a") as fh:
|
||||||
|
fh.write(self.TaskDict[KEY_GRAVER_PNG]+"\n")
|
||||||
|
fh.write(self.TaskDict[KEY_GRAVER_SVG]+"\n")
|
||||||
|
|
||||||
|
response.task = json.dumps(self.TaskDict, ensure_ascii=False)
|
||||||
|
self.TaskDict = {}
|
||||||
|
self._cstate = self.__TASK_COMPLETED
|
||||||
|
response.ok = True
|
||||||
|
return response
|
||||||
|
|
||||||
|
def takeon_task(self, request: TaskFromQueue.Request, response: TaskFromQueue.Response) -> TaskFromQueue.Response:
|
||||||
|
"""
|
||||||
|
Take a task from the queue for execution
|
||||||
|
"""
|
||||||
|
response.ok = False
|
||||||
|
if self.sz_queue and not self._cstate == self.__TASK_TAKE_ON: # очередь не пуста и нет текущего задания
|
||||||
|
with open(self.fn_queue, "r") as fh:
|
||||||
|
jdata = json.load(fh)
|
||||||
|
|
||||||
|
self.TaskDict = jdata[KEY_QUEUE].pop(0)
|
||||||
|
self.sz_queue = len(jdata)
|
||||||
|
|
||||||
|
with open(self.fn_queue, "w") as fh:
|
||||||
|
json.dump(jdata, fh, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
self._cstate = self.__TASK_TAKE_ON
|
||||||
|
response.ok = True
|
||||||
|
response.task = json.dumps(self.TaskDict, ensure_ascii=False)
|
||||||
|
self.get_logger().info(f"Task has been accepted for execution (TaskId={self.TaskDict['TaskId']})")
|
||||||
|
return response
|
||||||
|
|
||||||
|
def delete_tmp(self, fn_tmp:str) -> None:
|
||||||
|
"""
|
||||||
|
Delete temporary files from list in text file
|
||||||
|
"""
|
||||||
|
if os.path.isfile(fn_tmp):
|
||||||
|
with open(fn_tmp, "r+") as fh:
|
||||||
|
paths = fh.readlines()
|
||||||
|
fh.truncate(0) # Очищаем файл
|
||||||
|
|
||||||
|
# Удаляем пробелы и символы новой строки
|
||||||
|
paths = [path.strip() for path in paths]
|
||||||
|
# Удаляем файлы
|
||||||
|
for path in paths:
|
||||||
|
if os.path.isfile(path):
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().info(f"Ошибка при удалении файла {path}: {e}")
|
||||||
|
|
||||||
|
def init_queue(self) -> None:
|
||||||
|
"""
|
||||||
|
Init of tasks queue.
|
||||||
|
"""
|
||||||
|
# delete temporary files
|
||||||
|
self.delete_tmp(self.fn_tmp)
|
||||||
|
|
||||||
|
with open(self.fn_queue, "r") as fh:
|
||||||
|
jdata = json.load(fh)
|
||||||
|
queue = jdata[KEY_QUEUE]
|
||||||
|
self.sz_queue = len(queue)
|
||||||
|
if self.sz_queue > 0:
|
||||||
|
self.task_last_index = queue[self.sz_queue-1][KEY_TASK_INDEX]
|
||||||
|
|
||||||
|
def update_svg(self, svg_f:str, csv:Dict, task_id:int) -> Tuple[str, int, int]:
|
||||||
|
"""
|
||||||
|
Return updating SVG-file
|
||||||
|
"""
|
||||||
|
# Загружаем и парсим SVG как XML-файл
|
||||||
|
tree = ET.parse(svg_f)
|
||||||
|
root = tree.getroot()
|
||||||
|
# Размеры результирующего изображения
|
||||||
|
width = int(''.join(filter(str.isdigit, root.get("width")))) * FACTOR_mm_to_pixel
|
||||||
|
height = int(''.join(filter(str.isdigit, root.get("height")))) * FACTOR_mm_to_pixel
|
||||||
|
|
||||||
|
# Проходим по всем элементам в дереве
|
||||||
|
for elem in root.iter():
|
||||||
|
tag = get_tags_without_namespace(elem)
|
||||||
|
if tag == XMLTAG_FOR_REPLACE:
|
||||||
|
id = elem.get("id")
|
||||||
|
if id in csv: # поле есть в словаре
|
||||||
|
n = 0
|
||||||
|
for el_text in elem.iter(): # ищем двойные поля
|
||||||
|
tspan = get_tags_without_namespace(el_text)
|
||||||
|
if tspan == "tspan":
|
||||||
|
n += 1
|
||||||
|
if n == 1:
|
||||||
|
el_1 = el_text
|
||||||
|
else:
|
||||||
|
el_2 = el_text
|
||||||
|
|
||||||
|
val = str(csv[id]) # значение из словаря
|
||||||
|
if n > 1: # двойное поле
|
||||||
|
src_str = el_1.text # значение из шаблона (определяет максимальное кол-во символов в первой строке поля)
|
||||||
|
src_len = len(src_str)
|
||||||
|
# if src_len: # в случае, если шаблон заполнен
|
||||||
|
l = src_len+1 if src_len else len(val)/2 + 1
|
||||||
|
i_div = val[:l].rfind('-') # индекс для разделения строки
|
||||||
|
i_div = l-1 if i_div < 2 else i_div+1 # если нет '-'
|
||||||
|
el_1.text = val[:i_div]
|
||||||
|
el_2.text = val[i_div:]
|
||||||
|
self.get_logger().info(f"{id}: 1) '{el_1.text}' 2) '{el_2.text}'")
|
||||||
|
|
||||||
|
elif n == 1: #for el_text in elem.iter():
|
||||||
|
pre = el_1.text
|
||||||
|
el_1.text = val # Заменяем текст элемента на значение из словаря
|
||||||
|
self.get_logger().info(f"{id}: before: '{pre}' after '{el_1.text}'")
|
||||||
|
|
||||||
|
# Сохраняем изменённое дерево в новый SVG-файл
|
||||||
|
svg_f = os.path.join(self.workdir, f"tmpl_{task_id}.svg")
|
||||||
|
tree.write(svg_f, encoding='utf-8', xml_declaration=True)
|
||||||
|
|
||||||
|
return svg_f, width, height
|
||||||
|
|
||||||
|
def set_taskdata(self, src_d: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Setting up task data, filling in templates
|
||||||
|
"""
|
||||||
|
task_id = src_d[KEY_TASK_INDEX]
|
||||||
|
svg = src_d[KEY_GRAVER_TMPL]
|
||||||
|
svg_file = os.path.join(self.cfgdir, svg)
|
||||||
|
png = ""
|
||||||
|
if os.path.isfile(svg_file):
|
||||||
|
# Обработка SVG
|
||||||
|
svg_file, w, h = self.update_svg(svg_file, src_d[KEY_CSV_TASK], task_id)
|
||||||
|
# Конвертация SVG в PNG
|
||||||
|
png = os.path.join(self.workdir, f"tmpl_{task_id}.png")
|
||||||
|
# cairosvg.svg2png(url=svg_file, write_to=png)
|
||||||
|
cairosvg.svg2png(url=svg_file, write_to=png, output_width=w, output_height=h)
|
||||||
|
else:
|
||||||
|
svg_file = ""
|
||||||
|
|
||||||
|
src_d[KEY_GRAVER_PNG] = png
|
||||||
|
src_d[KEY_GRAVER_SVG] = svg_file
|
||||||
|
return src_d
|
||||||
|
|
||||||
|
def add_tasks(self, request: AddTasks.Request, response: AddTasks.Response) -> AddTasks.Response:
|
||||||
|
"""
|
||||||
|
Adding a task list to the queue
|
||||||
|
"""
|
||||||
|
fn = request.tasks_csv
|
||||||
|
if not os.path.isfile(fn):
|
||||||
|
response.ok = False
|
||||||
|
self.get_logger().error(f"No such task file: {fn}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
with open(self.fn_cfg, "r") as fh:
|
||||||
|
cfg = json.load(fh)
|
||||||
|
sz_cfg = len(cfg)
|
||||||
|
|
||||||
|
with open(self.fn_queue, "r") as fh:
|
||||||
|
queue_data = json.load(fh)
|
||||||
|
queue = queue_data[KEY_QUEUE]
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
# Считываем CSV-файл
|
||||||
|
data = pd.read_csv(fn, delimiter=',', skiprows=0, encoding="utf-8", keep_default_na=False)
|
||||||
|
# # Получаем количество строк
|
||||||
|
# num_rows = data.shape[0]
|
||||||
|
for index, row in data.iterrows():
|
||||||
|
if index == 0: # строка с наименованиями
|
||||||
|
continue
|
||||||
|
rd = row.to_dict()
|
||||||
|
icfg = int(rd[KEY_OBJECT_INDEX]) - 1
|
||||||
|
if icfg < sz_cfg and icfg >= 0:
|
||||||
|
self.task_last_index += 1
|
||||||
|
dict_res = self.set_taskdata({KEY_TASK_INDEX: self.task_last_index} | cfg[icfg] | {KEY_CSV_TASK: rd})
|
||||||
|
if dict_res[KEY_GRAVER_PNG]: # проверка на valid task
|
||||||
|
queue += [dict_res]
|
||||||
|
self.get_logger().info(f"Индекс: {index}, Данные: {dict_res}")
|
||||||
|
|
||||||
|
with open(self.fn_queue, "w") as fh:
|
||||||
|
# json.dump({KEY_QUEUE: queue}, fh, ensure_ascii=False)
|
||||||
|
json.dump({KEY_QUEUE: queue}, fh, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
self.sz_queue = len(queue)
|
||||||
|
|
||||||
|
response.ok = True
|
||||||
|
return response
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rclpy.init()
|
||||||
|
executor = rclpy.executors.SingleThreadedExecutor()
|
||||||
|
i_node = QueueService()
|
||||||
|
executor.add_node(i_node)
|
||||||
|
try:
|
||||||
|
executor.spin()
|
||||||
|
except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
|
||||||
|
i_node.destroy_node()
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Add table
Add a link
Reference in a new issue