用line_as_stream 简化流的读取

文件持久化技巧
本文介绍了一种确保文件中数组元素正确读写的技巧。通过使用line_as_stream方法来逐行读取数据,即使文件中出现损坏行,也能防止数据错位,确保其他元素的正确性。
<script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script> 在将数据持久化到文件时,你可能会发现很难强制要求系统将特定的部分数据写到一行中。将特定的数据写到同一行有时是很有用的,比如在你从流(如一个文件)中读取一个数组的时候。 假设你要读取一个数组的元素,其中有一行被破坏了(比如丢失了一些数据)。一般情况下,这会导致后面所有的元素都受损。 作为一个例子,假设我们有一个数据结构,是一个窗口数组,你希望把它持久化到一个文件中,象下面这样: 第一行:窗口的数量 后面的每一行都包含两个值:窗口的宽度和窗口的高度 写成代码似乎很简单: #include #include #include struct Window { Window( int nLength = 0, int nHeight = 0) : m_nWindowLength( nLength), m_nWindowHeight( nHeight) {} int m_nWindowLength; int m_nWindowHeight; }; std::ostream & operator << ( std::ostream & streamOut, const Window & value) { streamOut << value.m_nWindowLength << " " << value.m_nWindowHeight; return streamOut; } std::istream & operator >> ( std::istream & streamIn, Window & value) { streamIn >> value.m_nWindowLength >> value.m_nWindowHeight; return streamIn; } void write_windows( std::vector< Window> &aWindows, const char * strFileName) { std::ofstream streamOut( strFileName); // 第一行 streamOut << aWindows.size() << std::endl; // 其余行 std::vector< Window>::iterator itFirst = aWindows.begin(), itLast = aWindows.end(); while ( itFirst != itLast) { // 每个窗口的数据都在它自己那一行 streamOut << *itFirst << std::endl; itFirst; } } 但是,要正确地读出这些数据,可能会有一些问题: //可能出错!!! void read_windows( std::vector< Window> &aWindows, const char * strFileName) { aWindows.clear(); std::ifstream streamIn( strFileName); int nSize; streamIn >> nSize; for ( int idx = 0; idx < nSize; idx) { Window w; streamIn >> w; aWindows.push_back( w); } } 上面的代码并没有强制任何东西。所有数据都被放到一行中,这看起来没有什么问题。但如果用户不小心,修改了你的文件,插入了一个多余的值或删掉了一个值,那么后面所有的元素都会得到错误的值,而你的程序并不会意识到这一点。尝试运行一下下面的代码并仔细看看其中的注释: #include #include int main(int argc, char* argv[]) { std::vector< Window> aWindows; aWindows.push_back( Window( 100, 400)); aWindows.push_back( Window( 200, 400)); aWindows.push_back( Window( 400, 400)); aWindows.push_back( Window( 500, 500)); aWindows.push_back( Window( 600, 200)); aWindows.push_back( Window( 600, 400)); aWindows.push_back( Window( 600, 690)); write_windows( aWindows, "persist.txt"); std::vector< Window> aReadWindows; /* 在这里加一个调试断点; 修改persist.txt,删除第4行的第一个值*/ read_windows( aReadWindows, "persist.txt"); std::copy( aReadWindows.begin(), aReadWindows.end(), std::ostream_iterator< Window>( std::cout, "/n")); /*在这里加一个调试断点:看看你读了多少个错误的值! */ return 0; } 还好,你可以用来line_as_stream读取一行,然后将它看作一个流。用这种方法,你可以确定每个元素是从一行中读取的。于是,read_windows函数变成这样: void read_windows( std::vector< Window> &aWindows, const char * strFileName) { aWindows.clear(); std::ifstream streamIn( strFileName); int nSize; // 第一行 line_as_stream( streamIn) >> nSize; for ( int idx = 0; idx < nSize; idx) { Window w; //每个窗口的数据都在它自己那一行 line_as_stream( streamIn) >> w; aWindows.push_back( w); } } 现在,重新运行前面的例子,你可以看到只有一个元素受损,如你所料。 这就是line_as_stream的源码: #include #include #include namespace Private { template< class char_type, class char_traits> struct line_stream_holder { typedef line_stream_holder< char_type, char_traits> this_class; typedef std::basic_istringstream< char_type, char_traits> stream_type; typedef std::basic_string< char_type, char_traits> string_type; line_stream_holder( const string_type & value) : m_stream( value) {} line_stream_holder( const this_class & source) : m_stream( source.m_stream.str() ) {} // allow passing this stream in functions that // accept streams operator stream_type & () const { return m_stream; } private: mutable stream_type m_stream; }; template< class char_type, class char_traits, class value_type> inline typename line_stream_holder< char_type, char_traits>::stream_type & operator >> (const line_stream_holder< char_type, char_traits> & streamIn, value_type & value) { typedef typename line_stream_holder< char_type, char_traits>::stream_type stream_type; stream_type & underlyingStream = streamIn; underlyingStream >> value; return underlyingStream; } } // namespace Private template< class char_type, class char_traits> Private::line_stream_holder< char_type, char_traits> line_as_stream( std::basic_istream< char_type, char_traits> & streamIn, char_type chDelim = '/n') { std::basic_string< char_type, char_traits> strLine; std::getline( streamIn, strLine, chDelim); return strLine; }
/* * Copyright 2019 Yuta Hirokawa (University of Tsukuba, Japan) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <zstd.h> #include "./cbsl_internal.h" static cbsl_errors streaming_read_immediate(cbsl_ctx* ctx, void* data, uint64_t size) { CBSL_DEBUG_MESSAGE("%s: call\n", __func__); /* previous all decompressed data loaded */ CBSL_CHECK_COND_AND_RETURN(ctx->out_buffer_pos >= ctx->out_buffer_used, cbsl_error); ZSTD_outBuffer output = { data, size, 0 }; /* decompressed data stores directly */ while (output.pos < output.size) { /* previous read data from file */ if (ctx->in_buffer_pos < ctx->in_buffer_used) { /* input buffer has remained data from the previous decompression */ memmove(ctx->in_buffer, ctx->in_buffer + ctx->in_buffer_pos, ctx->in_buffer_used - ctx->in_buffer_pos); ctx->in_buffer_used -= ctx->in_buffer_pos; } else { ctx->in_buffer_used = 0ULL; } ctx->in_buffer_pos = 0ULL; ctx->in_buffer_used += fread(ctx->in_buffer + ctx->in_buffer_used, 1, ctx->in_buffer_size - ctx->in_buffer_used, ctx->fp); ZSTD_inBuffer input = { ctx->in_buffer, ctx->in_buffer_used, ctx->in_buffer_pos }; while (input.pos < input.size && output.pos < output.size) { const size_t ret = ZSTD_decompressStream(ctx->zstd_dctx, &output, &input); CBSL_CHECK_COND_MSG_AND_RETURN(!ZSTD_isError(ret), ZSTD_getErrorName(ret), cbsl_error); } ctx->in_buffer_used = input.size; ctx->in_buffer_pos = input.pos; } CBSL_ASSERT(output.pos == output.size); ctx->out_buffer_pos = output.pos; ctx->out_buffer_used = output.size; return cbsl_success; } static uint64_t read_from_buffer(cbsl_ctx* ctx, byte_t* data, uint64_t size) { uint64_t read = 0; if (ctx->out_buffer_pos < ctx->out_buffer_used) { const uint64_t read_size = MIN(ctx->out_buffer_used - ctx->out_buffer_pos, size); memcpy(data, ctx->out_buffer + ctx->out_buffer_pos, read_size); ctx->out_buffer_pos += read_size; read = read_size; } return read; } static cbsl_errors streaming_read_buffered(cbsl_ctx* ctx, void* data, uint64_t size) { CBSL_DEBUG_MESSAGE("%s: call\n", __func__); size_t zstd_ret = -1; /* memcpy from the previous decompression data */ uint64_t read = read_from_buffer(ctx, data, size); byte_t* pdata = (byte_t*)(data); /* streaming decompression and memcpy */ while (read < size) { if (ctx->in_buffer_pos < ctx->in_buffer_used) { /* input buffer has remained data from the previous decompression */ memmove(ctx->in_buffer, ctx->in_buffer + ctx->in_buffer_pos, ctx->in_buffer_used - ctx->in_buffer_pos); ctx->in_buffer_used -= ctx->in_buffer_pos; } else { /* input buffer is already consumed by the previous decompression */ ctx->in_buffer_used = 0; } ctx->in_buffer_pos = 0ULL; ctx->in_buffer_used += fread(ctx->in_buffer + ctx->in_buffer_used, 1, ctx->in_buffer_size - ctx->in_buffer_used, ctx->fp); if (ctx->in_buffer_used == 0) // 从文件中,读取不到数据了,无法解压 break; ZSTD_inBuffer input = { ctx->in_buffer, ctx->in_buffer_used, ctx->in_buffer_pos }; while (input.pos < input.size && read < size) { ZSTD_outBuffer output = { ctx->out_buffer, ctx->out_buffer_size, 0 }; zstd_ret = ZSTD_decompressStream(ctx->zstd_dctx, &output, &input); CBSL_CHECK_COND_MSG_AND_RETURN(!ZSTD_isError(zstd_ret), ZSTD_getErrorName(zstd_ret), cbsl_error); ctx->out_buffer_pos = 0ULL; ctx->out_buffer_used = output.pos; read += read_from_buffer(ctx, pdata + read, size - read); } ctx->in_buffer_pos = input.pos; ctx->in_buffer_used = input.size; if (zstd_ret == 0) { // zstd stream end ctx->zstd_file_end = 1; break; } } if (ctx->zstd_file_end == 1 && ctx->out_buffer_pos == ctx->out_buffer_used) ctx->zstd_buf_end = 1; CBSL_ASSERT(read == size || ctx->zstd_end == 1); ctx->line_buffer_read_from_zst = read; return cbsl_success; } cbsl_errors cbsl_read(cbsl_ctx* ctx, void* data, uint64_t size) { CBSL_CHECK_COND_AND_RETURN(ctx != NULL, cbsl_error); CBSL_CHECK_COND_AND_RETURN(ctx->mode == cbsl_load_mode, cbsl_error); CBSL_CHECK_COND_AND_RETURN(data != NULL, cbsl_error); CBSL_DEBUG_MESSAGE("read: %lu bytes\n", size); if (size > ctx->out_buffer_size) { /* data size is larger than compression buffer */ return streaming_read_immediate(ctx, data, size); } else { return streaming_read_buffered(ctx, data, size); } } // readline from stream cbsl_errors cbsl_readline(cbsl_ctx* ctx, char* data, uint64_t size) { cbsl_errors ret = cbsl_success; CBSL_CHECK_COND_AND_RETURN(ctx != NULL, cbsl_error); CBSL_CHECK_COND_AND_RETURN(ctx->mode == cbsl_load_mode, cbsl_error); CBSL_CHECK_COND_AND_RETURN(data != NULL, cbsl_error); CBSL_DEBUG_MESSAGE("%s: %lu bytes\n", __func__, size); uint64_t line_buffer_space = 0; line_buffer_space = CBSL_LINEBUF_SIZE - ctx->line_buffer_used; if (ctx->zstd_buf_end == 0) { if (line_buffer_space > ctx->out_buffer_size) { /* line_buffer size is larger than compression buffer */ ret = streaming_read_immediate(ctx, ctx->line_buffer + ctx->line_buffer_used, line_buffer_space); } else { ret = streaming_read_buffered(ctx, ctx->line_buffer + ctx->line_buffer_used, line_buffer_space); } ctx->line_buffer_used += ctx->line_buffer_read_from_zst; } uint64_t i; char* ptr; ptr = (char*)ctx->line_buffer; data[0] = '\0'; for (i = 0; i < ctx->line_buffer_used; i++) { if (*ptr == '\n' || *ptr == '\r') { CBSL_CHECK_COND_AND_RETURN(size >= i - 1, cbsl_error); memcpy(data, ctx->line_buffer, i + 1); memmove(ctx->line_buffer, ctx->line_buffer + i + 1, ctx->line_buffer_used - i - 1); ctx->line_buffer_used -= i + 1; data[i + 1] = '\0'; break; } ptr++; } if (ctx->line_buffer_used == 0) return cbsl_end; else return ret; }
06-30
#!/usr/bin/env python # -*- coding: UTF-8 -*- import rospy import time import cv2 import numpy as np import os import math import re from tta_m3e_rtsp.srv import takeoffOrLanding, gimbalControl from tta_m3e_rtsp.msg import flightByVel ########################################################################################################################################################## ########################################################################################################################################################## # 初始化服务进程 class UAVController: ############################## 记得改IP ###################### def __init__(self, ip_address='192.168.58.7'): rospy.init_node('uav_python_controller') # 初始化服务代理 rospy.wait_for_service('takeoffOrLanding') rospy.wait_for_service('gimbalControl') self.takeoff_landing_srv = rospy.ServiceProxy('takeoffOrLanding', takeoffOrLanding) self.gimbal_control_srv = rospy.ServiceProxy('gimbalControl', gimbalControl) # 初始化速度控制发布者 self.vel_pub = rospy.Publisher('flightByVel', flightByVel, queue_size=10) # 视频相关参数 self.ip_address = ip_address self.rtsp_url = f"rtsp://{self.ip_address}:8554/live" self.cap = None self.video_active = False # 微调参数 self.adjustment_threshold = 0.90 # 这是降落基准点 self.micro_adjust_speed = 0.1 # 这是微调移动速度 self.micro_adjust_time = 0.2 # 这是微调移动时间 self.descend_speed = 0.05 # 这是微调下降速度 # 图片保存路径 self.fruits_dir = "/home/forlinx/Pictures/Fruits" self.land_dir = "/home/forlinx/Pictures/Land" self.lines_dir = "/home/forlinx/Pictures/Lines" # 记录上一次降落的水果类型 self.last_fruit_type = None # 创建必要的目录 os.makedirs(self.fruits_dir, exist_ok=True) os.makedirs(self.land_dir, exist_ok=True) os.makedirs(self.lines_dir, exist_ok=True) rospy.loginfo("UAV控制器初始化完成") ########################################################################################################################################################## ########################################################################################################################################################## # 起飞 def takeoff(self, retries=3, interval=4): """带重试机制的起飞功能""" rospy.loginfo("等待10秒后起飞...") rospy.sleep(10) for attempt in range(retries): try: resp = self.takeoff_landing_srv(1) if resp.ack == 1: rospy.loginfo("起飞成功") return True else: rospy.logwarn(f"起飞失败,第{attempt+1}次重试...") rospy.sleep(interval) except rospy.ServiceException as e: rospy.logerr(f"起飞服务调用失败: {str(e)}") rospy.sleep(interval) rospy.logerr("起飞失败,请重启无人机") return False ########################################################################################################################################################## # 降落 def landing(self): """发送降落指令""" try: resp = self.takeoff_landing_srv(2) # 2表示降落 rospy.loginfo("降落指令发送成功,响应: %d", resp.ack) return resp.ack == 1 except rospy.ServiceException as e: rospy.logerr("降落服务调用失败: %s", str(e)) return False ########################################################################################################################################################## # 移动 def move_by_velocity(self, vel_x, vel_y, vel_z, fly_time=2.0, target_yaw=0): """通过速度控制无人机移动""" msg = flightByVel() msg.vel_x = vel_x msg.vel_y = vel_y msg.vel_z = vel_z msg.fly_time = fly_time msg.targetYaw = target_yaw self.vel_pub.publish(msg) rospy.loginfo("速度控制指令已发送: X=%.2f, Y=%.2f, Z=%.2f", vel_x, vel_y, vel_z) ########################################################################################################################################################## # 云台控制 def control_gimbal(self, pitch, roll=0, yaw=0): """控制云台角度""" try: resp = self.gimbal_control_srv(pitch, roll, yaw) rospy.loginfo("云台控制成功,角度: pitch=%.2f", pitch) return resp.ack == 1 except rospy.ServiceException as e: rospy.logerr("云台控制失败: %s", str(e)) return False ########################################################################################################################################################## # 云台校准与向右偏转 def gimbal_calibration_right_task(self): """云台校准任务(向右偏转)""" rospy.loginfo("开始云台校准任务(向右)") for _ in range(2): # 无人机右转45° self.move_by_velocity(0, 0, 0, 2.0, 45) rospy.sleep(2) # 云台向上35° self.control_gimbal(35) rospy.sleep(2) # 云台回中 self.control_gimbal(0) rospy.sleep(2) rospy.loginfo("云台校准任务(向右)完成") ########################################################################################################################################################## #云台校准与向左偏转 def gimbal_calibration_left_task(self): """云台校准任务(向左偏转)""" rospy.loginfo("开始云台校准任务(向左)") for _ in range(2): # 无人机左转45° self.move_by_velocity(0, 0, 0, 2.0, -45) rospy.sleep(2) # 云台向上35° self.control_gimbal(35) rospy.sleep(2) # 云台回中 self.control_gimbal(0) rospy.sleep(2) rospy.loginfo("云台校准任务(向左)完成") ########################################################################################################################################################## # 启动视频 def start_video_stream(self): """启动视频""" if not self.video_active: self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG) if not self.cap.isOpened(): rospy.logerr("无法打开摄像头,请检查IP地址或网络连接") return False self.video_active = True rospy.loginfo("视频已启动") return True ########################################################################################################################################################## # 关闭视频 def stop_video_stream(self): """停止视频""" if self.video_active and self.cap is not None: self.cap.release() self.video_active = False rospy.loginfo("视频已停止") ########################################################################################################################################################## # 拍照 def capture_and_save_image(self, image_name): """捕获并保存图像到指定目录""" if not self.video_active: self.start_video_stream() ret, frame = self.cap.read() if ret: filename = os.path.join(self.lines_dir, f"{image_name}.jpg") cv2.imwrite(filename, frame) rospy.loginfo(f"已保存图像: {filename}") return True return False ########################################################################################################################################################## # 检测水果 def detect_fruit_type(self, image_path): """根据图片文件名检测水果类型""" filename = os.path.basename(image_path) if "apple" in filename.lower(): return "apple" elif "watermelon" in filename.lower(): return "watermelon" elif "banana" in filename.lower(): return "banana" elif "orange" in filename.lower(): return "orange" return None ########################################################################################################################################################## # 寻找对应水果 def get_initial_movement(self, filename): """根据文件名获取初始移动方向和距离""" # 提取位置编号 (如1-1, 1-2等) match = re.search(r'(\d)-(\d)_', filename) if not match: return 0.0 group = int(match.group(1)) position = int(match.group(2)) # 根据位置编号确定移动距离 if position in [1, 5]: if "watermelon" in filename.lower(): return 3.6 elif "apple" in filename.lower(): return 2.8 elif "orange" in filename.lower(): return 1.5 elif "banana" in filename.lower(): return 0.0 elif position in [2, 6]: if "watermelon" in filename.lower(): return 2.3 elif "apple" in filename.lower(): return 1.3 elif "orange" in filename.lower(): return 0.0 elif "banana" in filename.lower(): return -1.6 elif position in [3, 7]: if "watermelon" in filename.lower(): return 1.0 elif "apple" in filename.lower(): return 0.0 elif "orange" in filename.lower(): return -1.6 elif "banana" in filename.lower(): return -2.8 elif position in [4, 8]: if "watermelon" in filename.lower(): return 0.0 elif "apple" in filename.lower(): return -1.0 elif "orange" in filename.lower(): return -2.3 elif "banana" in filename.lower(): return -3.6 return 0.0 ########################################################################################################################################################## # 寻找水果中心点 def process_frame_for_fruit(self, frame, fruit_type): """根据水果类型处理视频帧""" height, width = frame.shape[:2] center_x, center_y = width // 2, height // 2 cv2.drawMarker(frame, (center_x, center_y), (0, 0, 255), cv2.MARKER_CROSS, 20, 2) hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 根据水果类型设置颜色范围 if fruit_type == "apple": lower1 = np.array([0, 120, 80]) upper1 = np.array([10, 255, 255]) lower2 = np.array([160, 120, 80]) upper2 = np.array([180, 255, 255]) elif fruit_type == "watermelon": lower1 = np.array([35, 50, 30]) upper1 = np.array([90, 255, 200]) lower2 = upper2 = None elif fruit_type == "banana": lower1 = np.array([20, 150, 120]) upper1 = np.array([30, 255, 200]) lower2 = np.array([15, 130, 100]) upper2 = np.array([35, 255, 180]) elif fruit_type == "orange": lower1 = np.array([10, 120, 100]) upper1 = np.array([20, 255, 255]) lower2 = np.array([5, 100, 80]) upper2 = np.array([25, 255, 255]) else: return frame, None, (center_x, center_y), 0.0 # 创建颜色掩膜 mask1 = cv2.inRange(hsv, lower1, upper1) if lower2 is not None and upper2 is not None: mask2 = cv2.inRange(hsv, lower2, upper2) mask = cv2.bitwise_or(mask1, mask2) else: mask = mask1 # 形态学处理 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))) # 寻找最大轮廓 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) red_center = None red_area = 0 if contours: largest_contour = max(contours, key=cv2.contourArea) red_area = cv2.contourArea(largest_contour) if red_area > 100: # 最小面积阈值 # 绘制边界框 x, y, w, h = cv2.boundingRect(largest_contour) cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2) # 计算中心点 M = cv2.moments(largest_contour) if M["m00"] != 0: red_center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])) cv2.drawMarker(frame, red_center, (255, 0, 0), cv2.MARKER_CROSS, 20, 2) # 绘制中心点连线 cv2.line(frame, (center_x, center_y), red_center, (0, 255, 255), 2) # 计算距离和接近度 distance = math.sqrt((red_center[0]-center_x)**2 + (red_center[1]-center_y)**2) max_distance = math.sqrt(center_x**2 + center_y**2) closeness = 1 - (distance / max_distance) # 显示接近度 cv2.putText(frame, f"Closeness: {closeness:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) return frame, red_center, (center_x, center_y), closeness return frame, red_center, (center_x, center_y), 0.0 ########################################################################################################################################################## # 根据水果微调降落 def adjust_position_based_on_fruit(self, fruit_type): """根据水果类型调整无人机位置""" try: if not self.start_video_stream(): return False frame_count = 0 processed_frame = None fruit_center = None frame_center = None closeness = 0.0 start_time = time.time() while time.time() - start_time < 5: ret, frame = self.cap.read() if not ret: rospy.logerr("无法读取视频帧") continue frame_count += 1 if frame_count % 30 == 0: #每30帧处理一次视频 processed_frame, fruit_center, frame_center, closeness = self.process_frame_for_fruit(frame, fruit_type) if fruit_center is not None: if closeness >= self.adjustment_threshold: # 保存对准图像 filename = os.path.join(self.land_dir, f"Land_{fruit_type}.jpg") cv2.imwrite(filename, processed_frame) rospy.loginfo(f"已保存对准图像到: {filename}") self.stop_video_stream() # 降落前向前微调0.1米 rospy.loginfo("降落前向前微调0.1米") self.move_by_velocity(0.1, 0, 0, 1.0) rospy.sleep(1.5) self.landing() self.last_fruit_type = fruit_type return True dx = fruit_center[0] - frame_center[0] dy = fruit_center[1] - frame_center[1] distance = math.sqrt(dx**2 + dy**2) if distance > 0: norm_dx = dx / distance norm_dy = dy / distance else: norm_dx, norm_dy = 0, 0 vel_x = norm_dy * self.micro_adjust_speed vel_y = norm_dx * self.micro_adjust_speed vel_z = -self.descend_speed direction_x = "向前" if vel_x > 0 else "向后" if vel_x < 0 else "" direction_y = "向右" if vel_y > 0 else "向左" if vel_y < 0 else "" direction_z = "下降" directions = [] if abs(vel_x) > 0.01: directions.append(f"{direction_x} {abs(vel_x):.2f}m/s") if abs(vel_y) > 0.01: directions.append(f"{direction_y} {abs(vel_y):.2f}m/s") if abs(vel_z) > 0.01: directions.append(f"{direction_z} {abs(vel_z):.2f}m/s") direction_str = ", ".join(directions) if directions else "保持位置" rospy.loginfo(f"微调指令: {direction_str} | 接近度: {closeness:.2f}/{self.adjustment_threshold}") self.move_by_velocity(-vel_x, vel_y, vel_z, self.micro_adjust_time) rospy.sleep(0.1) self.stop_video_stream() rospy.sleep(4) if fruit_center is None: rospy.loginfo("本次采集周期未检测到目标水果") return False return False except Exception as e: rospy.logerr(f"处理视频帧时发生错误: {str(e)}") self.stop_video_stream() return False ########################################################################################################################################################## ########################################################################################################################################################## # 在小车停机坪上的降落 def process_frame(self, frame): """处理视频帧,寻找并标记最大红色区域,并标记中心点""" height, width = frame.shape[:2] center_x, center_y = width // 2, height // 2 cv2.drawMarker(frame, (center_x, center_y), (0, 0, 255), cv2.MARKER_CROSS, 20, 2) # 寻找红色区域 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) lower_red1 = np.array([0, 120, 70]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([170, 120, 70]) upper_red2 = np.array([180, 255, 255]) mask1 = cv2.inRange(hsv, lower_red1, upper_red1) mask2 = cv2.inRange(hsv, lower_red2, upper_red2) mask = cv2.bitwise_or(mask1, mask2) # 寻找最大轮廓 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) red_center = None red_area = 0 if contours: largest_contour = max(contours, key=cv2.contourArea) red_area = cv2.contourArea(largest_contour) if red_area > 100: # 最小面积阈值 x, y, w, h = cv2.boundingRect(largest_contour) cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2) M = cv2.moments(largest_contour) if M["m00"] != 0: red_center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])) cv2.drawMarker(frame, red_center, (255, 0, 0), cv2.MARKER_CROSS, 20, 2) cv2.line(frame, (center_x, center_y), red_center, (0, 255, 255), 2) distance = math.sqrt((red_center[0]-center_x)**2 + (red_center[1]-center_y)**2) max_distance = math.sqrt(center_x**2 + center_y**2) closeness = 1 - (distance / max_distance) cv2.putText(frame, f"Closeness: {closeness:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) return frame, red_center, (center_x, center_y), closeness return frame, red_center, (center_x, center_y), 0.0 ########################################################################################################################################################## ########################################################################################################################################################## # 小车红色区域微调的降落 def adjust_position_based_on_LandArea(self): """根据视频帧调整无人机位置""" try: # 创建Land图片保存目录 land_save_path = "/home/forlinx/Pictures/Land" os.makedirs(land_save_path, exist_ok=True) if not self.start_video_stream(): return False frame_count = 0 processed_frame = None red_center = None frame_center = None closeness = 0.0 start_time = time.time() while time.time() - start_time < 5: ret, frame = self.cap.read() if not ret: rospy.logerr("无法读取视频帧") continue frame_count += 1 if frame_count % 30 == 0: #每30帧处理一次视频换言之即为 每30帧控制一次飞机微调 processed_frame, red_center, frame_center, closeness = self.process_frame(frame) if red_center is not None: if closeness >= self.adjustment_threshold: # 修改保存路径到Land文件夹 filename = os.path.join(land_save_path, "Land.jpg") cv2.imwrite(filename, processed_frame) rospy.loginfo(f"已保存对准图像到: {filename}") self.stop_video_stream() self.landing() return True dx = red_center[0] - frame_center[0] dy = red_center[1] - frame_center[1] distance = math.sqrt(dx**2 + dy**2) if distance > 0: norm_dx = dx / distance norm_dy = dy / distance else: norm_dx, norm_dy = 0, 0 vel_x = norm_dy * self.micro_adjust_speed vel_y = norm_dx * self.micro_adjust_speed vel_z = -self.descend_speed direction_x = "向前" if vel_x > 0 else "向后" if vel_x < 0 else "" direction_y = "向右" if vel_y > 0 else "向左" if vel_y < 0 else "" direction_z = "下降" directions = [] if abs(vel_x) > 0.01: directions.append(f"{direction_x} {abs(vel_x):.2f}m/s") if abs(vel_y) > 0.01: directions.append(f"{direction_y} {abs(vel_y):.2f}m/s") if abs(vel_z) > 0.01: directions.append(f"{direction_z} {abs(vel_z):.2f}m/s") direction_str = ", ".join(directions) if directions else "保持位置" rospy.loginfo(f"微调指令: {direction_str} | 接近度: {closeness:.2f}/{self.adjustment_threshold}") self.move_by_velocity(-vel_x, vel_y, vel_z, self.micro_adjust_time) rospy.sleep(0.1) self.stop_video_stream() rospy.sleep(4) if red_center is None: rospy.loginfo("本次采集周期未检测到红色区域") return False return False except Exception as e: rospy.logerr(f"处理视频帧时发生错误: {str(e)}") self.stop_video_stream() return False ########################################################################################################################################################## ########################################################################################################################################################## # 从处理的文件夹内判断降落水果 def run_fruit_mission(self, line_name): """执行水果降落任务,只处理与航线名称对应的图片""" rospy.loginfo(f"开始执行{line_name}的水果降落任务") # 1. 检查Fruits目录是否存在 if not os.path.exists(self.fruits_dir): rospy.logerr(f"水果图片目录不存在: {self.fruits_dir}") return # 2. 获取目录中与航线名称匹配的图片文件 fruit_files = [f for f in os.listdir(self.fruits_dir) if f.lower().startswith(line_name.lower()) and f.lower().endswith(('.jpg', '.jpeg', '.png'))] if not fruit_files: rospy.logerr(f"未找到与航线{line_name}匹配的水果图片") return fruit_file = fruit_files[0] fruit_path = os.path.join(self.fruits_dir, fruit_file) fruit_type = self.detect_fruit_type(fruit_path) if not fruit_type: rospy.logerr(f"无法从文件名识别水果类型: {fruit_file}") return rospy.loginfo(f"识别到水果类型: {fruit_type}, 文件: {fruit_file}") # 3. 根据水果类型微调位置并降落 self.adjust_position_based_on_fruit(fruit_type) rospy.loginfo(f"{line_name}水果降落任务完成") ########################################################################################################################################################## ########################################################################################################################################################## # ============================================= # 航线1系列任务(完整修改版) # ============================================= def line_A1_1(self): """航线A1-1任务""" rospy.loginfo("执行A1-1飞行路线") # 向上0.25米 self.move_by_velocity(0, 0, 0.25) rospy.sleep(2) # 向左0.72米 self.move_by_velocity(0, -0.72, 0) rospy.sleep(2) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-1") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-1") def line_A1_2(self): """航线A1-2任务""" rospy.loginfo("执行A1-2飞行路线") # 向右0.72米 self.move_by_velocity(0, 0.72, 0) rospy.sleep(2) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-2") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-2") def line_A1_3(self): """航线A1-3任务""" rospy.loginfo("执行A1-3飞行路线") # 向上0.25米 self.move_by_velocity(0, 0, 0.25) rospy.sleep(2) # 向右1.87米 self.move_by_velocity(0, 1.87, 0) rospy.sleep(3) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-3") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-3") def line_A1_4(self): """航线A1-4任务""" rospy.loginfo("执行A1-4飞行路线") # 向上0.3米 self.move_by_velocity(0, 0, 0.3) rospy.sleep(2) # 向右3.0米 self.move_by_velocity(0, 3.0, 0) rospy.sleep(4) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-4") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-4") def line_A1_5(self): """航线A1-5任务""" rospy.loginfo("执行A1-5飞行路线") # 向下0.3米 self.move_by_velocity(0, 0, -0.3) rospy.sleep(2) # 向左0.72米 self.move_by_velocity(0, -0.72, 0) rospy.sleep(2) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-5") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-5") def line_A1_6(self): """航线A1-6任务""" rospy.loginfo("执行A1-6飞行路线") # 向下0.3米 self.move_by_velocity(0, 0, -0.3) rospy.sleep(2) # 向右0.72米 self.move_by_velocity(0, 0.72, 0) rospy.sleep(2) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-6") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-6") def line_A1_7(self): """航线A1-7任务""" rospy.loginfo("执行A1-7飞行路线") # 向下0.3米 self.move_by_velocity(0, 0, -0.3) rospy.sleep(2) # 向右2.0米 self.move_by_velocity(0, 2.0, 0) rospy.sleep(3) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-7") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-7") def line_A1_8(self): """航线A1-8任务""" rospy.loginfo("执行A1-8飞行路线") # 向下0.3米 self.move_by_velocity(0, 0, -0.3) rospy.sleep(2) # 向右3.0米 self.move_by_velocity(0, 3.0, 0) rospy.sleep(4) # 向后0.35米 self.move_by_velocity(-0.35, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("A1-8") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_right_task() # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("A1-8") # ============================================= # 航线2系列任务(完整修改版) # ============================================= def line_B2_1(self): """航线B2-1任务""" rospy.loginfo("执行B2-1飞行路线") # 向上0.2米 self.move_by_velocity(0, 0, 0.2) rospy.sleep(2) # 向后0.2米 self.move_by_velocity(-0.2, 0, 0) rospy.sleep(2) # 向右2.95米 self.move_by_velocity(0, 2.95, 0) rospy.sleep(4) # 拍摄并保存图片 self.capture_and_save_image("B2-1") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-1") def line_B2_2(self): """航线B2-2任务""" rospy.loginfo("执行B2-2飞行路线") # 向上0.2米 self.move_by_velocity(0, 0, 0.2) rospy.sleep(2) # 向后0.2米 self.move_by_velocity(-0.2, 0, 0) rospy.sleep(2) # 向右1.6米 self.move_by_velocity(0, 1.6, 0) rospy.sleep(3) # 拍摄并保存图片 self.capture_and_save_image("B2-2") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-2") def line_B2_3(self): """航线B2-3任务""" rospy.loginfo("执行B2-3飞行路线") # 向上0.2米 self.move_by_velocity(0, 0, 0.2) rospy.sleep(2) # 向后0.2米 self.move_by_velocity(-0.2, 0, 0) rospy.sleep(2) # 向右0.6米 self.move_by_velocity(0, 0.6, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("B2-3") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-3") def line_B2_4(self): """航线B2-4任务""" rospy.loginfo("执行B2-4飞行路线") # 向上0.2米 self.move_by_velocity(0, 0, 0.2) rospy.sleep(2) # 向后0.2米 self.move_by_velocity(-0.2, 0, 0) rospy.sleep(2) # 向左0.5米 self.move_by_velocity(0, -0.5, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("B2-4") rospy.sleep(15) # 向上0.8米 self.move_by_velocity(0, 0, 0.8) rospy.sleep(2) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-4") def line_B2_5(self): """航线B2-5任务""" rospy.loginfo("执行B2-5飞行路线") # 向下0.2米 self.move_by_velocity(0, 0, -0.2) rospy.sleep(2) # 向右3.0米 self.move_by_velocity(0, 3.0, 0) rospy.sleep(4) # 向后0.5米 self.move_by_velocity(-0.5, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("B2-5") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-5") def line_B2_6(self): """航线B2-6任务""" rospy.loginfo("执行B2-6飞行路线") # 向下0.3米 self.move_by_velocity(0, 0, -0.3) rospy.sleep(2) # 向右1.7米 self.move_by_velocity(0, 1.7, 0) rospy.sleep(3) # 向后0.5米 self.move_by_velocity(-0.5, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("B2-6") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-6") def line_B2_7(self): """航线B2-7任务""" rospy.loginfo("执行B2-7飞行路线") # 向下0.3米 self.move_by_velocity(0, 0, -0.3) rospy.sleep(2) # 向右0.5米 self.move_by_velocity(0, 0.5, 0) rospy.sleep(2) # 向后0.5米 self.move_by_velocity(-0.5, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("B2-7") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-7") def line_B2_8(self): """航线B2-8任务""" rospy.loginfo("执行B2-8飞行路线") # 向下0.3米 self.move_by_velocity(0, 0, -0.3) rospy.sleep(2) # 向左0.5米 self.move_by_velocity(0, -0.5, 0) rospy.sleep(2) # 向后0.5米 self.move_by_velocity(-0.5, 0, 0) rospy.sleep(2) # 拍摄并保存图片 self.capture_and_save_image("B2-8") rospy.sleep(15) # 向上1.2米 self.move_by_velocity(0, 0, 1.2) rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_left_task() # 向右0.7米 self.move_by_velocity(0, 0.7, 0) rospy.sleep(2) # 执行水果降落任务 self.run_fruit_mission("B2-8") # ============================================= # 过渡任务 # ============================================= def transition_operation_1(self): """过渡操作一""" rospy.loginfo("开始执行过渡操作一") # 重新起飞 if not self.takeoff(): return False # 向右2.8米 self.move_by_velocity(0, 2.8, 0) rospy.sleep(3) # 向下1.6米 self.move_by_velocity(0, 0, -1.6) rospy.sleep(3) # 根据上一次降落水果类型移动 if self.last_fruit_type == "apple": self.move_by_velocity(0.3, 0, 0) # 向前飞0.3米 elif self.last_fruit_type == "watermelon": self.move_by_velocity(-0.3, 0, 0) # 向后飞0.3米 elif self.last_fruit_type == "orange": self.move_by_velocity(1.8, 0, 0) # 向前飞1.8米 elif self.last_fruit_type == "banana": self.move_by_velocity(3.1, 0, 0) # 向前飞3.1米 rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_right_task() # 等待10秒 rospy.sleep(10) rospy.loginfo("过渡操作一完成") def transition_operation_2(self): """过渡操作二""" rospy.loginfo("开始执行过渡操作二") # 重新起飞 if not self.takeoff(): return False # 向左0.7米 self.move_by_velocity(0, -0.7, 0) rospy.sleep(2) # 向下1.6米 self.move_by_velocity(0, 0, -1.6) rospy.sleep(3) # 根据上一次降落水果类型移动 if self.last_fruit_type == "apple": self.move_by_velocity(0.3, 0, 0) # 向前飞0.3米 elif self.last_fruit_type == "watermelon": self.move_by_velocity(-0.3, 0, 0) # 向后飞0.3米 elif self.last_fruit_type == "orange": self.move_by_velocity(1.8, 0, 0) # 向前飞1.8米 elif self.last_fruit_type == "banana": self.move_by_velocity(3.1, 0, 0) # 向前飞3.1米 rospy.sleep(3) # 执行云台校准 self.gimbal_calibration_right_task() # 等待5秒 rospy.sleep(5) rospy.loginfo("过渡操作二完成") ########################################################################################################################################################## # 最终降落 def run_land_mission(self): """执行降落任务""" rospy.loginfo("开始执行降落任务") # 控制云台向下90° self.control_gimbal(-90) rospy.sleep(2) # 启动视频并微调位置 if self.start_video_stream(): while not rospy.is_shutdown(): if self.adjust_position_based_on_LandArea(): break rospy.sleep(0.1) self.stop_video_stream() # 在最终降落前向前飞行0.1米 rospy.loginfo("降落前向前微调0.1米") self.move_by_velocity(0.1, 0, 0, 1.0) rospy.sleep(1.5) # 最终降落 if not self.landing(): rospy.logerr("最终降落失败") return rospy.loginfo("降落任务完成") ########################################################################################################################################################## # 执行航线 def run_mission(self, line_names): """执行航线任务""" # 带重试的起飞功能 if not self.takeoff(): return # 执行航线任务 for i, line_name in enumerate(line_names): # 航线1系列任务 if line_name == "A1-1": self.line_A1_1() elif line_name == "A1-2": self.line_A1_2() elif line_name == "A1-3": self.line_A1_3() elif line_name == "A1-4": self.line_A1_4() elif line_name == "A1-5": self.line_A1_5() elif line_name == "A1-6": self.line_A1_6() elif line_name == "A1-7": self.line_A1_7() elif line_name == "A1-8": self.line_A1_8() # 航线2系列任务 elif line_name == "B2-1": self.line_B2_1() elif line_name == "B2-2": self.line_B2_2() elif line_name == "B2-3": self.line_B2_3() elif line_name == "B2-4": self.line_B2_4() elif line_name == "B2-5": self.line_B2_5() elif line_name == "B2-6": self.line_B2_6() elif line_name == "B2-7": self.line_B2_7() elif line_name == "B2-8": self.line_B2_8() # 如果不是最后一个任务,执行过渡飞行 if i < len(line_names) - 1: # 判断下一个任务是1系列还是2系列 next_task = line_names[i+1] if next_task.startswith('A1-'): self.transition_to_line1() elif next_task.startswith('B2-'): self.transition_to_line2() # 如果是2系列最后一个任务,执行红色区域降落 if line_names[-1].startswith('B2-'): self.run_land_mission() rospy.loginfo("所有航线任务执行完成") ########################################################################################################################################################## ########################################################################################################################################################## if __name__ == '__main__': try: controller = UAVController() line_names = [] # 解析命令行参数 for param in rospy.myargv(): if "LineName=" in param: line_names = param.split("=")[1].split(",") break if not line_names: rospy.logerr("未指定航线名称,请使用 LineName=A1-1,B2-1 格式指定") else: controller.run_mission(line_names) except rospy.ROSInterruptException: controller.stop_video_stream() rospy.loginfo("程序中断") 优化
07-31
接口代码如下:# !/usr/bin/env python # -*- coding: utf-8 -*- # 版权信息:华为技术有限公司,版本所有(C) 2025-2099 """ 功 能:供应链 SCM Agent -- interface/roles/order/supply_manager_assistant_app_langgraph-供应经理助手fastapi接口 """ import json import os import sys from time import time import uvicorn from aipaas.logger_factory import logger from fastapi import Request, FastAPI from fastapi.responses import StreamingResponse from langchain_core.messages import HumanMessage from infrastructure.auth_fastapi import SoaAuth from infrastructure.langfuse_telemetery.trace_langgraph import create_langfuse_callback from scm_agent.src.application.roles.order.supply_manager_assistant_graph.main_graph.graph import \ get_supply_manager_assistant_main_graph from scm_agent.src.common.agent_name import AgentName from scm_agent.src.common.constants import Status from scm_agent.src.infrastructures.agent_config_download.config_download import dowload_agent_config_langgraph from scm_agent.src.infrastructures.agent_config_read.read_yaml_config import read_project_config from scm_agent.src.infrastructures.agent_state.agent_state_helper import get_redis_key from scm_agent.src.infrastructures.app_postprocess.output_process import str_to_output_json from scm_agent.src.infrastructures.app_postprocess.output_process import str_to_stream_output_langgraph from scm_agent.src.infrastructures.memory.postgre_checkpointer.postgre_checkpointer import FrameworkAdapter from scm_agent.src.infrastructures.read_config import app_config from scm_agent.src.interface.input_output_parameters import SupplyManagerAssistantChatInput, ConfigUpdateInput, \ ConfigUpdateOutput os.environ['NO_PROXY'] = '127.0.0.1,localhost' fastapi_app = FastAPI(lifespan=FrameworkAdapter.lifespan_wrapper) env = os.environ.get("env") soa = SoaAuth(env_type=env, skip_soa_auth=False, only_check_token=True) agent_name = AgentName.SupplyManagerAssistantLangGraph project_config = {} common_prompt_config = {} def preload_agent_config(name): """ 预加载agent配置 Args: name: 助手/技能名称,app_config中配置 """ global project_config global common_prompt_config dowload_agent_config_langgraph(name) # 读取项目配置文件 project_config = read_project_config(agent_name, f"{agent_name}.yaml").get(env) # 读取公共Prompt配置文件 common_prompt_config = read_project_config("common", "prompt_config.yaml") if 'PYCHARM_HOSTED' in os.environ or 'PYCHARM_DEBUG_PROCESS' in os.environ: logger.info("debug模式请在此打断点") # raise Exception("debug模式请在此打断点,注释此行即可") preload_agent_config(agent_name) async def generator(graph, supply_manager_assistant_chat_input, initial_state, config): yield str_to_stream_output_langgraph('<think>') yield str_to_stream_output_langgraph('**问题**') question = supply_manager_assistant_chat_input.question.strip() yield str_to_stream_output_langgraph('\n' + question) async for chunk in graph.astream( input=initial_state, stream_mode="custom", config=config, subgraphs=True ): yield str_to_stream_output_langgraph(chunk[1]) @fastapi_app.post('/roles/supply_manager_assistant_chat_langgraph') @soa.required async def supply_manager_assistant_chat(request: Request, supply_manager_assistant_chat_input: SupplyManagerAssistantChatInput): # checkpointer = presit_param.get("checkpointer") checkpointer = request.app.state.presist_param.get("checkpointer") thread_id = get_redis_key(supply_manager_assistant_chat_input) user_id = supply_manager_assistant_chat_input.user_id session_id = supply_manager_assistant_chat_input.session_id langfuse_callback = create_langfuse_callback(user_id=user_id, session_id=session_id, trace_name=AgentName.SupplyManagerAssistantLangGraph) config = {"configurable": {"thread_id": thread_id}, "metadata": {"user_id": supply_manager_assistant_chat_input.user_id, "project_config": project_config, "common_prompt_config": common_prompt_config, "ctx_params": supply_manager_assistant_chat_input.ctxParams}, "callbacks": [langfuse_callback], } try: graph = get_supply_manager_assistant_main_graph(checkpointer) initial_state = {"messages": [HumanMessage(content=supply_manager_assistant_chat_input.question)]} return StreamingResponse( generator(graph, supply_manager_assistant_chat_input, initial_state, config), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"} ) except Exception as e: return str_to_output_json(f'处理异常,异常原因: {e}') @fastapi_app.post('/config_update') @soa.required async def config_update(request: Request, config_update_input: ConfigUpdateInput): start_time = time() config_update_output = ConfigUpdateOutput() try: preload_agent_config(config_update_input.agent_name) config_update_output.status = Status.SUCCESS except Exception as e: config_update_output.error_message = "[SCM-Agent] Update config error." # 耗时统计 config_update_output.elapsed_time = str(time() - start_time) return config_update_output.to_dict() @fastapi_app.get('/health') @soa.required async def health(request: Request, ): return json.dumps({"success": True}, ensure_ascii=False) if __name__ == '__main__': uvicorn.run("supply_manager_assistant_app_langgraph:fastapi_app", host=app_config.get('host', '0.0.0.0'), # port=app_config.get('port', 8080), loop="asyncio", port=8080) # workers=app_config.get('workers', 4)) postgre_checkpointer.py目前没问题了,但是现在接口代码报错如下:ERROR: Exception in ASGI application + Exception Group Traceback (most recent call last): | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 426, in run_asgi | result = await app( # type: ignore[func-returns-value] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 84, in __call__ | return await self.app(scope, receive, send) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\fastapi\applications.py", line 1054, in __call__ | await super().__call__(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\applications.py", line 123, in __call__ | await self.middleware_stack(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__ | raise exc | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__ | await self.app(scope, receive, _send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 65, in __call__ | await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\_exception_handler.py", line 64, in wrapped_app | raise exc | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app | await app(scope, receive, sender) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\routing.py", line 756, in __call__ | await self.middleware_stack(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\routing.py", line 776, in app | await route.handle(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\routing.py", line 297, in handle | await self.app(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\routing.py", line 77, in app | await wrap_app_handling_exceptions(app, request)(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\_exception_handler.py", line 64, in wrapped_app | raise exc | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app | await app(scope, receive, sender) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\routing.py", line 75, in app | await response(scope, receive, send) | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\responses.py", line 258, in __call__ | async with anyio.create_task_group() as task_group: | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\anyio\_backends\_asyncio.py", line 772, in __aexit__ | raise BaseExceptionGroup( | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) +-+---------------- 1 ---------------- | Traceback (most recent call last): | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\responses.py", line 261, in wrap | await func() | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\starlette\responses.py", line 250, in stream_response | async for chunk in self.body_iterator: | File "D:\code\iscp-app-aigc-ai\scm_agent\src\interface\roles\order\supply_manager_assistant_app_langgraph.py", line 71, in generator | async for chunk in graph.astream( | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\langgraph\pregel\main.py", line 2883, in astream | async with AsyncPregelLoop( | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\langgraph\pregel\_loop.py", line 1186, in __aenter__ | saved = await self.checkpointer.aget_tuple(self.checkpoint_config) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\langgraph\checkpoint\postgres\aio.py", line 192, in aget_tuple | async with self._cursor() as cur: | File "C:\Users\zwx1453293\AppData\Local\Programs\Python\Python311\Lib\contextlib.py", line 204, in __aenter__ | return await anext(self.gen) | ^^^^^^^^^^^^^^^^^^^^^ | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\langgraph\checkpoint\postgres\aio.py", line 388, in _cursor | async with conn.cursor(binary=True, row_factory=dict_row) as cur: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\psycopg\connection_async.py", line 256, in cursor | self._check_connection_ok() | File "D:\code\iscp-app-aigc-ai\.venv\Lib\site-packages\psycopg\_connection_base.py", line 528, in _check_connection_ok | raise e.OperationalError("the connection is closed") | psycopg.OperationalError: the connection is closed +------------------------------------ 之前接口代码是没问题的
09-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值