瀏覽代碼

修改代码:增加系统运行模式开关;完善socket链路相关代码增加业务健壮性;修改sokcet心跳业务逻辑,实时显示基站状态。

zhoutao 3 月之前
父節點
當前提交
f5490b569a

+ 16 - 3
ipsomcadmin/src/components/AppTable/index.vue

@@ -24,14 +24,22 @@
 
         <div class="app-table">
             <template v-if="table">
-                <el-table v-loading="tableData.loading" :data="tableData.items" border :default-sort="tableDefaultSort"
-                    size="small" @expand-change="tableExpandChange">
+                <el-table v-if="slotWhere == 0" v-loading="tableData.loading" :data="tableData.items" border
+                    :default-sort="tableDefaultSort" size="small" @expand-change="tableExpandChange">
                     <el-table-column type="index" label="序号" width="64" align="center"></el-table-column>
                     <el-table-column v-for="column in tableData.columns" :prop="column.key" :label="column.name"
                         sortable :key="column.key" :formatter="tableData.formatter[column.key]" :width="column.width"
                         header-align="center"></el-table-column>
                     <slot name="table-column"></slot>
                 </el-table>
+                <el-table v-else-if="slotWhere == 1" v-loading="tableData.loading" :data="tableData.items" border
+                    :default-sort="tableDefaultSort" size="small" @expand-change="tableExpandChange">
+                    <el-table-column type="index" label="序号" width="64" align="center"></el-table-column>
+                    <slot name="table-column"></slot>
+                    <el-table-column v-for="column in tableData.columns" :prop="column.key" :label="column.name"
+                        sortable :key="column.key" :formatter="tableData.formatter[column.key]" :width="column.width"
+                        header-align="center"></el-table-column>
+                </el-table>
             </template>
             <template v-else>
                 <slot name="table"></slot>
@@ -120,7 +128,6 @@ export default {
             type: Function,
             default: null
         },
-
         table: Boolean,
         tableData: {
             type: Object,
@@ -134,6 +141,12 @@ export default {
                 return { prop: 'id', order: 'descending' }
             }
         },
+        slotWhere: {//操作列位置,0:后面,1:前面
+            type: Number,
+            default: function () {
+                return 0
+            }
+        }
     },
     computed: {
         align() {

+ 6 - 6
ipsomcadmin/src/views/bparam/baseparam.vue

@@ -88,12 +88,12 @@
             </el-form>
         </app-search>
         <app-table pagination paginationAlign="right" table :tableData="tableData.listData"
-            :handlePageSizeChange="handlePageSizeChange" :handlePageNumChange="handlePageNumChange">
+            :handlePageSizeChange="handlePageSizeChange" :handlePageNumChange="handlePageNumChange" :slotWhere=1>
             <template slot="table-column">
-                <el-table-column label="操作" header-align="center" width="100" testalign="center">
+                <el-table-column label="操作" header-align="center" width="80" testalign="center">
                     <template slot-scope="scope">
-                        <el-dropdown>
-                            <span class="el-dropdown-link">
+                        <el-dropdown trigger="click">
+                            <span class="el-dropdown-link" style="font-size:13px;cursor: pointer;padding-right: 10px;">
                                 操作<i class="el-icon-arrow-down el-icon--right"></i>
                             </span>
                             <el-dropdown-menu>
@@ -253,7 +253,7 @@ export default {
                     },
                     columns: [
                         { key: 'project_id', name: '项目编号', width: 100 },
-                        { key: 'bts_id', name: '基站ID', width: 100 },
+                        { key: 'bts_id', name: '基站编号', width: 100 },
                         { key: 'bts_type', name: '类型', width: 80 },
                         { key: 'soft_version', name: '版本', width: 80 },
                         { key: 'enable_flag', name: '使能', width: 80 },
@@ -265,7 +265,7 @@ export default {
                         { key: 'coord_x', name: 'X坐标', width: 100 },
                         { key: 'coord_y', name: 'Y坐标', width: 100 },
                         { key: 'coord_z', name: 'Z坐标', width: 100 },
-                        { key: 'position', name: '安装位置' },
+                        { key: 'position', name: '位置' },
                     ],
                     formatter: {
                         'project_id': this.formatProjectId,

+ 4 - 4
ipsomcadmin/src/views/bparam/component/netparamdlg.vue

@@ -60,18 +60,18 @@
             <el-form-item label="wifi密码">
                 <el-input v-model="dialogData.editData.data.wifi_pwd" auto-complete="off"></el-input>
             </el-form-item>
-            <el-form-item label="管理中心URL">
+            <el-form-item label="socket地址">
                 <div class="netparamdlg">
                     <div class="netparamdlg__item">
                         <div class="netparamdlg__item-left">
                             <el-input v-model="dialogData.editData.data.service_ip" auto-complete="off"
                                 placeholder="www.xxx.com或者ip地址"></el-input>
                         </div>
-                        <div class="netparamdlg__item-right">管理中心的域名或者IP地址</div>
+                        <div class="netparamdlg__item-right">socket服务器地址,可域名也可IP</div>
                     </div>
                 </div>
             </el-form-item>
-            <el-form-item label="管理中心端口">
+            <el-form-item label="socket端口">
                 <div class="netparamdlg">
                     <div class="netparamdlg__item">
                         <div class="netparamdlg__item-left">
@@ -79,7 +79,7 @@
                                 v-model.trim="dialogData.editData.data.service_port" auto-complete="off"
                                 style="width:100%"></el-input-number>
                         </div>
-                        <div class="netparamdlg__item-right">管理中心在此端口监听链接</div>
+                        <div class="netparamdlg__item-right">socket服务器监听端口</div>
                     </div>
                 </div>
             </el-form-item>

+ 14 - 4
ipsomcapi/core/dao/mysql/mysqlapi/mysqlapi.go

@@ -26,7 +26,8 @@ var (
 )
 
 // 初始化数据库模块,创建一个数据连接对象
-func OpenSqlDb() error {
+// sysWorkMode:系统工作模式,0:开发模式,1:生成模式
+func OpenSqlDb(sysWorkMode uint32) error {
 	var err error
 
 	myViper := util.GetViper()
@@ -46,10 +47,19 @@ func OpenSqlDb() error {
 		time.Sleep(5 * time.Second)
 		Db, err = gorm.Open("mysql", dsn)
 	}
-	Db.LogMode(true)       //打开日志开关
-	Db.SingularTable(true) //禁用数据库表名复数
 
-	TimerConnectSqlDb(Db) //5秒定时连接数据库,以免连接断开
+	//设置日志开关
+	if sysWorkMode == 0 {
+		Db.LogMode(true) //打开日志
+	} else {
+		Db.LogMode(false) //关闭日志
+	}
+
+	//禁用数据库表名复数
+	Db.SingularTable(true)
+
+	//30秒定时连接数据库,以免连接断开
+	TimerConnectSqlDb(Db)
 
 	return nil
 }

+ 12 - 2
ipsomcapi/core/router/router.go

@@ -17,9 +17,19 @@ import (
 )
 
 // 配置路由
-func SetRouter(runMode string) *gin.Engine {
+// runMode:gin框架运行模式
+// sysWorkMode:系统工作模式,0:开发模式,1:生产模式
+func SetRouter(runMode string, sysWorkMode uint32) *gin.Engine {
+	var r *gin.Engine
 	gin.SetMode(runMode) //设置运行模式
-	r := gin.Default()
+
+	//配置系统工作模式
+	if sysWorkMode == 0 {
+		r = gin.Default() //打开调试记录
+	} else {
+		r = gin.New()         // 使用gin.New()代替gin.Default(),不包含Logger中间件
+		r.Use(gin.Recovery()) // 手动添加Recovery中间件(处理 panic)
+	}
 
 	/******************************以下是PC端业务模块路由注册*****************************/
 	groupRouter := r.Group("/pcapi")

+ 15 - 4
ipsomcapi/main.go

@@ -8,22 +8,30 @@ import (
 	"ipsomc/core/router"
 	"ipsomc/module/socket/socketcreate"
 	"ipsomc/module/timer"
+	"ipsomc/public"
 	"ipsomc/util"
 
 	"github.com/gin-gonic/gin"
 	_ "github.com/jinzhu/gorm/dialects/mysql"
 )
 
+// socket模块
 var socketCreateApi socketcreate.SocketCreate
 
+// 系统工作模式,0:开发模式,1:生产模式
+var sysWorkMode uint32 = 1
+
 func main() {
 	ctx := context.Background()
 
+	//设置系统工作模式
+	public.PublicSetSysWorkMode(sysWorkMode)
+
 	//初始化配置文件
 	util.ViperConfigInit("config", "config")
 
 	//创建定时器
-	go timer.CreateTimer()
+	go timer.CreateTimer(ctx)
 
 	//创建socket监听
 	//go socketCreateApi.CreateUdpSocket()
@@ -34,11 +42,14 @@ func main() {
 	defer redisapi.CloseRds()
 
 	//打开MySql数据库
-	mysqlapi.OpenSqlDb()
+	mysqlapi.OpenSqlDb(sysWorkMode)
 	defer mysqlapi.CloseSqlDb()
 
 	//注册并启动路由
-	r := router.SetRouter(gin.DebugMode)
-	//r := router.SetRouter(gin.ReleaseMode)
+	runMode := gin.ReleaseMode //发布模式,
+	if sysWorkMode == 0 {
+		runMode = gin.DebugMode //调试模式
+	}
+	r := router.SetRouter(runMode, sysWorkMode)
 	r.Run(":8080") //监听
 }

+ 46 - 0
ipsomcapi/module/bparam/bparamhandler/bparamhandler.go

@@ -124,6 +124,29 @@ func (obj *BparamHan) GetBtsBaseParamListHan(c *gin.Context) {
 		return
 	}
 
+	//更新基站在线状态
+	existFlag := false
+	if len(public.Gpub_mapHeart) > 0 {
+		//更新基站状态
+		for i, btsParam := range dataList {
+			existFlag = false
+
+			//修改基站状态
+			for _, v := range public.Gpub_mapHeart {
+				if btsParam.ProjectID == v.ProjectID && btsParam.BtsID == v.BtsID {
+					dataList[i].Status = 2 //在线
+					existFlag = true
+					break
+				}
+			}
+
+			if !existFlag && btsParam.Status == 2 {
+				dataList[i].Status = 1 //离线
+			}
+
+		}
+	}
+
 	//返回记录
 	resp.RespList(c, dataList)
 }
@@ -146,6 +169,29 @@ func (obj *BparamHan) GetBtsBaseParamPageListHan(c *gin.Context) {
 		return
 	}
 
+	//更新基站在线状态
+	existFlag := false
+	if len(public.Gpub_mapHeart) > 0 {
+		//更新基站状态
+		for i, btsParam := range dataList {
+			existFlag = false
+
+			//修改基站状态
+			for _, v := range public.Gpub_mapHeart {
+				if btsParam.ProjectID == v.ProjectID && btsParam.BtsID == v.BtsID {
+					dataList[i].Status = 2 //在线
+					existFlag = true
+					break
+				}
+			}
+
+			if !existFlag && btsParam.Status == 2 {
+				dataList[i].Status = 1 //离线
+			}
+
+		}
+	}
+
 	resp.RespList(c, pageListData)
 }
 

+ 2 - 13
ipsomcapi/module/oam/oamreport.go

@@ -5,7 +5,6 @@
 package oam
 
 import (
-	"fmt"
 	"ipsomc/module/ps/psmodel"
 	"ipsomc/public"
 )
@@ -24,18 +23,8 @@ const (
 // wLen:队列长度
 func (obj *OamReport) OamReportCmd(stHeadModel *psmodel.PS_HEAD_T, wCommand uint16, dataList []byte, wLen uint16) error {
 	switch wCommand {
-	case ORDER_REPORT_UDP_HEART:
-		var modeHeart public.HeartMapValue
-		heartIdStr := fmt.Sprintf("%08X", stHeadModel.StVpHead.ProjectID) + "-" + fmt.Sprintf("%08X", stHeadModel.StVpHead.DeviceID)
-		_, exist := public.Gpub_mapHeart[heartIdStr]
-		if !exist {
-			modeHeart.ProjectID = stHeadModel.StVpHead.ProjectID
-			modeHeart.BtsID = stHeadModel.StVpHead.DeviceID
-
-			public.Gpub_mutex.Lock()
-			public.Gpub_mapHeart[heartIdStr] = modeHeart //在线
-			public.Gpub_mutex.Unlock()
-		}
+	case ORDER_REPORT_UDP_HEART: //心跳包
+		public.PublicAddItem(stHeadModel.StVpHead.ProjectID, stHeadModel.StVpHead.DeviceID)
 	default:
 		break
 	}

+ 38 - 21
ipsomcapi/module/socket/socketcreate/socketcreate.go

@@ -12,6 +12,7 @@ import (
 	"ipsomc/module/ps/psmodel"
 	"ipsomc/module/ps/psul"
 	"ipsomc/module/socket/socketsend"
+	"ipsomc/public"
 	"ipsomc/util"
 	"net"
 	"os"
@@ -85,6 +86,7 @@ func (obj *SocketCreate) CreateUdpSocket() {
 
 // 创建TCP Socket
 func (obj *SocketCreate) CreateTcpSocket() {
+
 	myViper := util.GetViper()
 	hostaddress := myViper.GetString("socket.hostaddress")     //主机地址
 	hostport := myViper.GetInt("socket.hostport")              //主机端口
@@ -121,6 +123,8 @@ func (obj *SocketCreate) CreateTcpSocket() {
 // 处理每一个TCP连接
 func (obj *SocketCreate) HandleTcpConnection(conn *net.TCPConn) {
 	defer conn.Close()
+	// conn.SetKeepAlive(false)
+	// conn.SetKeepAlivePeriod(5 * time.Minute) // 每隔5分钟检测连接
 
 	//保存tcp连接句柄
 	address := conn.RemoteAddr().String()
@@ -132,36 +136,49 @@ func (obj *SocketCreate) HandleTcpConnection(conn *net.TCPConn) {
 	for {
 		tmpBuffer := make([]byte, 1024)  //临时缓存
 		n, err := reader.Read(tmpBuffer) //从客户端读取数据
-		if err != nil && err != io.EOF {
-			fmt.Println("Error reading:", err.Error())
-			obj.socketSendApi.DeleteTcpConnHandle(address) //删除一个tcp连接
+		if err != nil {
+			if err == io.EOF {
+				fmt.Println("客户端已关闭链接:", err.Error())
+			} else {
+				fmt.Println("从客户端读取数据错误:", err.Error())
+			}
+			obj.socketSendApi.DeleteTcpConnHandle(address)
 			break
 		}
 		dataBuffer = append(dataBuffer, tmpBuffer[:n]...) //把数据追加到数据缓存中
 
 		//成帧处理,并且把数据发送给协议栈
 		for len(dataBuffer) > psmodel.PS_FRAME_MIN_LEN { //数据长度大于最小帧长
-			if dataBuffer[0] == psmodel.PS_AP_END_FLAG {
-				// 查找数据包的结尾
-				endIndex := bytes.IndexByte(dataBuffer[1:], psmodel.PS_AP_END_FLAG)
-				if endIndex != -1 {
-					// 找到了完整的数据包
-					dataPacket := dataBuffer[0 : endIndex+2] // 包括结尾的0x7E
-					dataBuffer = dataBuffer[endIndex+2:]     // 移除已处理的数据包,包括结尾的0x7E
+			// 查找第一个合法帧头(0x7E)
+			startIndex := bytes.IndexByte(dataBuffer, psmodel.PS_AP_END_FLAG)
+			if startIndex == -1 {
+				// 无合法帧头,清空缓存
+				dataBuffer = dataBuffer[:0]
+				break
+			} else if startIndex > 0 {
+				// 丢弃帧头前的无效数据
+				dataBuffer = dataBuffer[startIndex:]
+			}
 
-					//开启goroutine,将数据报文发送给协议栈模块
-					go obj.psUlApi.PsUlTcp(address, dataPacket, len(dataPacket))
+			// 继续查找帧尾(第二个0x7E)
+			endIndex := bytes.IndexByte(dataBuffer[1:], psmodel.PS_AP_END_FLAG)
+			if endIndex == -1 {
+				// 未找到帧尾,等待更多数据
+				break
+			}
 
-					fmt.Printf("Received %d bytes from %s\n", len(dataPacket), address)
-				} else {
-					// 没有找到结尾,等待更多数据
-					break
-				}
-			} else {
-				// 丢弃非法数据(数据以0x7E开头),并等待更多数据
-				dataBuffer = dataBuffer[1:]
+			// 提取完整数据包(包含头尾的0x7E)
+			dataPacket := dataBuffer[0 : endIndex+2]
+			dataBuffer = dataBuffer[endIndex+2:]
+
+			//开启goroutine,将数据报文发送给协议栈模块
+			go obj.psUlApi.PsUlTcp(address, dataPacket, len(dataPacket))
+
+			//获得系统工作模式
+			sysWorkMode := public.PublicGetSysWorkMode()
+			if sysWorkMode == 0 {
+				fmt.Printf("Received %d bytes from %s\n", len(dataPacket), address)
 			}
 		}
-
 	}
 }

+ 28 - 4
ipsomcapi/module/socket/socketsend/socketsend.go

@@ -11,27 +11,37 @@ import (
 	"ipsomc/module/socket/socketmodel"
 	"ipsomc/public"
 	"net"
+	"strconv"
+	"sync"
+	"time"
 )
 
 // 定义全局变量
 var gSocket_UdpConn *net.UDPConn = nil                 // UDP句柄
 var GSocket_mapTcpConn = make(map[string]*net.TCPConn) //TCP连接map
+var mapMutex sync.RWMutex
 
 type SocketSend struct {
 }
 
 // udp连接句柄
 func (obj *SocketSend) SaveUdpConnHandle(udpConn *net.UDPConn) {
+	mapMutex.Lock()
+	defer mapMutex.Unlock()
 	gSocket_UdpConn = udpConn
 }
 
 // 保存TCP连接句柄
 func (obj *SocketSend) SaveTcpConnHandle(tcpConn *net.TCPConn, address string) {
+	mapMutex.Lock()
+	defer mapMutex.Unlock()
 	GSocket_mapTcpConn[address] = tcpConn
 }
 
 // 删除TCP连接句柄
 func (obj *SocketSend) DeleteTcpConnHandle(address string) {
+	mapMutex.Lock()
+	defer mapMutex.Unlock()
 	delete(GSocket_mapTcpConn, address)
 }
 
@@ -73,10 +83,19 @@ func (obj *SocketSend) SendDataToBtsTcp(projectId int, btsId int, apDataFrame []
 	var clientAddrModel socketmodel.ClientAddr //客户端地址
 
 	//组织redis建值,并查询客户端IP与端口
+	var maxRetries = 3
+	var existFlag = 0
 	redisKey := fmt.Sprintf("%08X", int(projectId)) + "-" + fmt.Sprintf("%08X", int(btsId)) //KEY
-	if err := redisObj.GetModelData(redisKey, &clientAddrModel); err != nil {
-		//删除map中的一个元素
-		public.PublicDeleteOneItem(redisKey)
+	for i := 0; i < maxRetries; i++ {
+		if err := redisObj.GetModelData(redisKey, &clientAddrModel); err == nil {
+			existFlag = 1 //找到了
+			break
+		}
+		time.Sleep(1 * time.Second)
+	}
+	//若不存在
+	if existFlag == 0 {
+		public.PublicDeleteOneItem(redisKey) //删除map中的一个元素
 		return errors.New("该链接已过期")
 	}
 
@@ -89,7 +108,12 @@ func (obj *SocketSend) SendDataToBtsTcp(projectId int, btsId int, apDataFrame []
 			return err
 		}
 
-		println("send data to bts", apDataFrame[0], apDataFrame[len(apDataFrame)-1], len(apDataFrame))
+		//获得系统工作模式
+		sysWorkMode := public.PublicGetSysWorkMode()
+		if sysWorkMode == 0 {
+			text := "Send " + strconv.Itoa(len(apDataFrame)) + " bytes to " + clientAddrModel.Address
+			println(text)
+		}
 	}
 
 	return nil

+ 26 - 4
ipsomcapi/module/timer/timer.go

@@ -5,25 +5,47 @@
 package timer
 
 import (
+	"context"
 	"fmt"
 	"ipsomc/module/bparam/bparamapi"
 	"ipsomc/public"
+	"sync/atomic"
 	"time"
 )
 
 var bparamApi bparamapi.BparamApi
+var gTimerCounter int32 = 0 //定时器触发计数器
 
 // 创建一个定时器
-func CreateTimer() {
-	ticker := time.NewTicker(6 * time.Minute)
+func CreateTimer(ctx context.Context) {
+	ticker := time.NewTicker(5 * time.Minute)
 	defer ticker.Stop()
 
+	//获得系统工作模式
+	sysWorkMode := public.PublicGetSysWorkMode()
+
 	// 使用无限循环等待Ticker的触发
 	for {
 		select {
+		case <-ctx.Done():
+			return // 收到退出信号时终止
 		case t := <-ticker.C:
-			fmt.Println("Ticker触发时间:", t)
-			bparamApi.UpdateBtsStatus(public.Gpub_mapHeart) //修改基站状态
+			if sysWorkMode == 0 {
+				fmt.Println("定时器触发时间:", t.Format("2006-01-02 15:16:17"))
+			}
+
+			newVal := atomic.AddInt32(&gTimerCounter, 1)
+
+			//每15分钟维护一次心跳map(删除心跳map中的数据)
+			if newVal%3 == 0 {
+				public.PublicDeleteAllItem()
+			}
+
+			//每两小时更新一次数据库
+			if newVal >= 24 {
+				atomic.StoreInt32(&gTimerCounter, 0)
+				go bparamApi.UpdateBtsStatus(public.Gpub_mapHeart) //修改基站状态
+			}
 		}
 	}
 }

+ 51 - 8
ipsomcapi/public/public.go

@@ -4,9 +4,12 @@
 
 package public
 
-import "sync"
+import (
+	"fmt"
+	"sync"
+)
 
-//定义网管模块编号
+// 定义网管模块编号
 const (
 	BTS_MODULE_DEVICE  = 1 //设备参数
 	BTS_MODULE_REPORT  = 2 //上报参数
@@ -18,7 +21,7 @@ const (
 	BTS_MODULE_RTC     = 8 //RTC参数
 )
 
-//定义MCP层命令类型
+// 定义MCP层命令类型
 const (
 	BTS_MCP_REPORT = 1 //基站参数上报
 	BTS_MCP_QUERY  = 2 //基站参数参数
@@ -37,7 +40,7 @@ type PublicUpgradeStatus struct {
 	Percent uint8 `json:"percent"` //百分比
 }
 
-//全局变量
+// 全局变量
 var (
 	Gpub_mutex   sync.Mutex
 	Gpub_mapChan = make(map[string]chan interface{}) //保存每个http请求的chan
@@ -47,16 +50,56 @@ var (
 
 	//web socket消息通道
 	Gpub_chanWebSocketMsg = make(chan PublicUpgradeStatus)
+
+	Gpub_sysWorkMode uint32 = 0 //系统工作模式,0:调试模式,1:生产模式
 )
 
-//删除map中的一个元素
-func PublicDeleteOneItem(keyStr string) {
-	delete(Gpub_mapHeart, keyStr)
+// 增加一个心跳map元素
+// projectId:项目编号
+// btsId:基站编号
+// 返回值:0:map中不存在该元素,1:map中已存在该元素
+func PublicAddItem(projectId int, btsId int) int {
+	var result int = 1 //返回值
+	var modeHeart HeartMapValue
+	modeHeart.ProjectID = projectId
+	modeHeart.BtsID = btsId
+
+	//组织mapKey字符
+	mapKey := fmt.Sprintf("%08X", projectId) + "-" + fmt.Sprintf("%08X", btsId)
+	_, exist := Gpub_mapHeart[mapKey]
+	if !exist { //map中部存在该key
+		Gpub_mutex.Lock()
+		defer Gpub_mutex.Unlock()
+		Gpub_mapHeart[mapKey] = modeHeart //增加一个map元素
+		result = 0
+	}
+
+	return result
+}
+
+// 删除map中的一个元素
+func PublicDeleteOneItem(mapKey string) {
+	Gpub_mutex.Lock()
+	defer Gpub_mutex.Unlock()
+	delete(Gpub_mapHeart, mapKey)
 }
 
-//删除map中的所有元素
+// 删除map中的所有元素
 func PublicDeleteAllItem() {
+	Gpub_mutex.Lock()
+	defer Gpub_mutex.Unlock()
+
 	for key := range Gpub_mapHeart {
 		delete(Gpub_mapHeart, key)
 	}
 }
+
+// 设置系统工作模式
+func PublicSetSysWorkMode(sysWorkMode uint32) {
+	Gpub_sysWorkMode = sysWorkMode
+}
+
+// 获得系统工作模式
+func PublicGetSysWorkMode() uint32 {
+	return Gpub_sysWorkMode
+}