root 4 päivää sitten
vanhempi
commit
69e7792de1

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1046 - 0
internal/app/ami/action/call.go


+ 122 - 0
internal/app/ami/action/channel.go

@@ -0,0 +1,122 @@
+package action
+
+import (
+	"errors"
+	"pbx-api-gin/api/model"
+	"pbx-api-gin/pkg/lfshook"
+	"pbx-api-gin/pkg/utils"
+)
+
+// CoreShowChannels 获取通话通道
+func CoreShowChannels() (result []model.CoreShowChannelResVO, err error) {
+	// 通过 src 查询对应通道
+	_, events, err := AminInstance.Send(map[string]string{
+		"Action": "CoreShowChannels",
+	})
+	if err != nil {
+		lfshook.NewLogger().Errorf("core show channels error %+v", err)
+		return nil, err
+	}
+
+	lfshook.NewLogger().Infof("events %+v", events)
+	result = make([]model.CoreShowChannelResVO, 0)
+	for _, event := range events {
+		if event.Data["Event"] == "CoreShowChannel" {
+			channel := model.CoreShowChannelResVO{
+				CallerIDName:      event.Data["CallerIDName"],
+				CallerIDNum:       event.Data["CallerIDNum"],
+				ConnectedLineName: event.Data["ConnectedLineName"],
+				ConnectedLineNum:  event.Data["ConnectedLineNum"],
+				Channel:           event.Data["Channel"],
+				Duration:          event.Data["Duration"],
+				DurationSecond:    utils.TimeStringToSecond(event.Data["Duration"]),
+				ChannelStateDesc:  event.Data["ChannelStateDesc"],
+			}
+			result = append(result, channel)
+		}
+	}
+	lfshook.NewLogger().Infof("channels %+v", result)
+	return result, nil
+}
+
+// GetChannelByExten  通过 exten 查询对应通道
+func GetChannelByExten(exten string) (channel string, err error) {
+	lfshook.NewLogger().Infof("GetChannelByExten %s", exten)
+	_, events, err := AminInstance.Send(map[string]string{
+		"Action": "CoreShowChannels",
+	})
+	if err != nil {
+		lfshook.NewLogger().Errorf("core show channels error %+v", err)
+		return "", err
+	}
+
+	for _, event := range events {
+		lfshook.NewLogger().Infof("CoreShowChannels event Data %+v", event.Data)
+		if event.Data["Event"] == "CoreShowChannel" && event.Data["ConnectedLineNum"] == exten && event.Data["CallerIDNum"] != exten {
+			channel = event.Data["Channel"]
+			lfshook.NewLogger().Infof("GetChannelByExten get channel %s", channel)
+			break
+		}
+	}
+	if channel == "" {
+		lfshook.NewLogger().Errorf("not found channel %s", exten)
+		return "", errors.New("not found channel")
+	}
+
+	return channel, nil
+}
+
+// GetChannelByExten  通过 exten 查询对应通道
+func GetChannelByExtenNotBridged(exten string) (channel string, err error) {
+	lfshook.NewLogger().Infof("GetChannelByExten %s", exten)
+	_, events, err := AminInstance.Send(map[string]string{
+		"Action": "CoreShowChannels",
+	})
+
+	if err != nil {
+		lfshook.NewLogger().Errorf("core show channels error %+v", err)
+		return "", err
+	}
+
+	for _, event := range events {
+		//lfshook.NewLogger().Infof("CoreShowChannels event Data %+v", event.Data)
+		if event.Data["Event"] == "CoreShowChannel" && event.Data["CallerIDNum"] == exten {
+			channel = event.Data["Channel"]
+			lfshook.NewLogger().Infof("GetChannelByExten get channel %s", channel)
+			break
+		}
+	}
+	if channel == "" {
+		lfshook.NewLogger().Errorf("not found channel %s", exten)
+		return "", errors.New("not found channel")
+	}
+
+	return channel, nil
+}
+
+// GetBridgedChan  通过 exten 查询对应通道
+func GetExtenChan(exten string) (channel string, err error) {
+	lfshook.NewLogger().Infof("GetExtenChan %s", exten)
+	_, events, err := AminInstance.Send(map[string]string{
+		"Action": "CoreShowChannels",
+	})
+	if err != nil {
+		lfshook.NewLogger().Errorf("core show channels error %+v", err)
+		return "", err
+	}
+
+	for _, event := range events {
+		//	lfshook.NewLogger().Infof("CoreShowChannels event Data %+v", event.Data)
+		if event.Data["Event"] == "CoreShowChannel" && event.Data["ConnectedLineNum"] != exten && event.Data["CallerIDNum"] == exten {
+			channel = event.Data["Channel"]
+			lfshook.NewLogger().Infof("GetChannelByExten get channel %s", channel)
+			break
+		}
+	}
+	if channel == "" {
+		lfshook.NewLogger().Errorf("not found channel %s", exten)
+		return "", errors.New("not found channel")
+	}
+
+	return channel, nil
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1613 - 0
internal/app/ami/action/index.go


+ 52 - 0
internal/app/ami/action/playback.go

@@ -0,0 +1,52 @@
+package action
+
+import (
+	"errors"
+	"fmt"
+	alstatus "pbx-api-gin/internal/app/stc/sendstatus"
+	"pbx-api-gin/pkg/lfshook"
+	"strings"
+)
+
+func PlaybackPacu(filename string, count int, delay int, PaType string) (err error) {
+
+	Para := fmt.Sprintf("count=%d,filename=%s,delay=%d,priority=%s", count, strings.Replace(strings.Replace(filename, ".mp3", "", -1), ".wav", "", -1), delay, PaType)
+	Chan := ""
+
+	switch PaType {
+	case "STN":
+		Chan = "Local/0503" //spa-rule
+	case "SPC":
+		Chan = "Local/0505" //spc-rule
+	case "DCS":
+		Chan = "Local/0504" //dc-rule
+	case "CHK":
+		Chan = "Local/0510" //paging pacu
+	case "EMG":
+		Chan = "Local/0502" //emg-rule
+	case "VOL":
+		Chan = "Local/0513" //emg-rule
+	}
+
+	action := map[string]string{
+		"Action":   "Originate",
+		"Channel":  Chan,
+		"Exten":    "000",
+		"CallerID": PaType,
+		"Context":  "broadcast-playfile",
+		"Priority": "1",
+		"Variable": Para,
+	}
+
+	lfshook.NewLogger().Logger.Infof("PlaybackPacu action : %+v", action)
+
+	res, _, err := AminInstance.Send(action)
+	if err != nil {
+		return err
+	}
+	if res["Response"] != "Success" {
+		alstatus.PaStatus("", PaType, "fail")
+		return errors.New(res["Message"])
+	}
+	return nil
+}

+ 43 - 0
internal/app/ami/action/queue.go

@@ -0,0 +1,43 @@
+package action
+
+import (
+	"pbx-api-gin/internal/app/ami/model"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// QueueStatus 队列信息
+func QueueStatus(queue, member string) (queueParams *model.QueueParams, err error) {
+	action := map[string]string{
+		"Action": "QueueStatus",
+	}
+	if queue != "" {
+		action["Queue"] = queue
+	}
+	if member != "" {
+		action["Member"] = member
+	}
+	_, events, _ := AminInstance.Send(action)
+	for _, event := range events {
+		if event.Data["Event"] == "QueueParams" {
+			queueParams = &model.QueueParams{Members: make([]*model.QueueMember, 0), Entrys: make([]*model.QueueEntry, 0)}
+			mapstructure.Decode(event.Data, queueParams)
+			continue
+		}
+		if event.Data["Event"] == "QueueMember" {
+			member := &model.QueueMember{}
+			mapstructure.Decode(event.Data, member)
+			queueParams.Members = append(queueParams.Members, member)
+			continue
+		}
+
+		if event.Data["Event"] == "QueueEntry" {
+			entry := &model.QueueEntry{}
+			mapstructure.Decode(event.Data, entry)
+			queueParams.Entrys = append(queueParams.Entrys, entry)
+			continue
+		}
+	}
+	//lfshook.NewLogger().Infof("================QueueStatus calls:%v===========", queueParams.Calls)
+	return queueParams, nil
+}

+ 36 - 0
internal/app/ami/model/cdr.go

@@ -0,0 +1,36 @@
+package model
+
+// Cdr 通话记录
+type Cdr struct {
+	AnswerTime         string `xorm:"AnswerTime"`
+	BillableSeconds    string `xorm:"BillableSeconds"`
+	CallerID           string `xorm:"CallerID"`
+	Channel            string `xorm:"Channel"`
+	Destination        string `xorm:"Destination"`
+	DestinationChannel string `xorm:"DestinationChannel"`
+	DestinationContext string `xorm:"DestinationContext"`
+	Disposition        string `xorm:"Disposition"`
+	Duration           string `xorm:"Duration"`
+	EndTime            string `xorm:"EndTime"`
+	Event              string `xorm:"Event"`
+	LastApplication    string `xorm:"LastApplication"`
+	LastData           string `xorm:"LastData"`
+	Privilege          string `xorm:"Privilege"`
+	RecordFile         string `xorm:"RecordFile"`
+	Source             string `xorm:"Source"`
+	StartTime          string `xorm:"StartTime"`
+	Timestamp          string `xorm:"Timestamp"`
+	UniqueID           string `xorm:"UniqueID"`
+}
+
+type RcdConf struct {
+	PadRcdEnable      string `json:"padRcdEnable"`
+	PadRcdStorageDays string `json:"padRcdStorageDays"`
+	PaRcdStorageDays  string `json:"paRcdStorageDays"`
+	CpaRcdStorageDays string `json:"cpaRcdStorageDays"`
+	OpaRcdStorageDays string `json:"opaRcdStorageDays"`
+	PadRcdDelDays     string `json:"pad_rcd_del_days"`
+	PaRcdDelDays      string `json:"pa_rcd_del_days"`
+	CpaRcdDelDays     string `json:"cpa_rcd_del_days"`
+	OpaRcdDelDays     string `json:"opa_rcd_del_days"`
+}

+ 40 - 0
internal/app/ami/model/dial.go

@@ -0,0 +1,40 @@
+package model
+
+type Dial struct {
+	Event                 string `json:"event"`
+	Channel               string `json:"channel"`
+	ChannelState          string `json:"channelState"`
+	ChannelStateDesc      string `json:"channelStateDesc"`
+	CallerIDNum           string `json:"callerIDNum"`
+	CallerIDName          string `json:"callerIDName"`
+	ConnectedLineNum      string `json:"connectedLineNum"`
+	ConnectedLineName     string `json:"connectedLineName"`
+	AccountCode           string `json:"accountCode"`
+	Context               string `json:"context"`
+	Exten                 string `json:"exten"`
+	Priority              string `json:"priority"`
+	Uniqueid              string `json:"uniqueid"`
+	Linkedid              string `json:"linkedid"`
+	DestChannel           string `json:"destChannel"`
+	DestChannelState      string `json:"destChannelState"`
+	DestChannelStateDesc  string `json:"destChannelStateDesc"`
+	DestCallerIDNum       string `json:"destCallerIDNum"`
+	DestCallerIDName      string `json:"destCallerIDName"`
+	DestConnectedLineNum  string `json:"destConnectedLineNum"`
+	DestConnectedLineName string `json:"destConnectedLineName"`
+	DestLanguage          string `json:"destLanguage"`
+	DestAccountCode       string `json:"destAccountCode"`
+	DestContext           string `json:"destContext"`
+	DestExten             string `json:"destExten"`
+	DestPriority          string `json:"destPriority"`
+	DestUniqueid          string `json:"destUniqueid"`
+	DestLinkedid          string `json:"destLinkedid"`
+	DialString            string `json:"dialString"`
+	Forward               string `json:"forward"`
+}
+
+type DialBeign Dial
+
+type DialEnd Dial
+
+type DialState Dial

+ 84 - 0
internal/app/ami/model/event.go

@@ -0,0 +1,84 @@
+package model
+
+import "strings"
+
+type PresenceStateChange struct {
+	Event      string `json:"event"`
+	Status     string `json:"status"`
+	Message    string `json:"message"`
+	Presentity string `json:"presentity"`
+	Extension  string `json:"extension"`
+	DndStatus  string `json:"dndStatus"`
+}
+
+type Hangup struct {
+	AccountCode       string `json:"accountCode"`
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Cause             string `json:"cause"`
+	Causetxt          string `json:"causetxt"`
+	Channel           string `json:"channel"`
+	ChannelState      string `json:"channelState"`
+	ChannelStateDesc  string `json:"channelStateDesc"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Context           string `json:"context"`
+	Event             string `json:"event"`
+	Exten             string `json:"exten"`
+	Linkedid          string `json:"linkedid"`
+	Priority          string `json:"priority"`
+	Privilege         string `json:"privilege"`
+	Timestamp         string `json:"timestamp"`
+	Uniqueid          string `json:"uniqueid"`
+}
+
+type Newstate struct {
+	AccountCode       string `json:"accountCode"`
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ChannelState      string `json:"channelState"`
+	ChannelStateDesc  string `json:"channelStateDesc"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Context           string `json:"context"`
+	Event             string `json:"event"`
+	Exten             string `json:"exten"`
+	Language          string `json:"language"`
+	Linkedid          string `json:"linkedid"`
+	Priority          string `json:"priority"`
+	Privilege         string `json:"privilege"`
+	Timestamp         string `json:"timestamp"`
+	Uniqueid          string `json:"uniqueid"`
+}
+
+type SuccessfulAuth struct {
+	Event         string `json:"event"`
+	AccountID     string `json:"accountID"`
+	RemoteAddress string `json:"remoteAddress"`
+	Address       string `json:"address"`
+}
+
+func (event *SuccessfulAuth) GetAddress() {
+	data := strings.Split(event.RemoteAddress, "/")
+	if len(data) == 4 {
+		event.Address = data[2]
+	} else {
+		event.Address = event.RemoteAddress
+	}
+}
+
+type MessageWaiting struct {
+	Event     string `json:"event"`
+	Extension string `json:"extension"`
+	New       string `json:"new"`
+	Old       string `json:"old"`
+	Mailbox   string `json:"mainbox"`
+}
+
+func (event *MessageWaiting) GetExtension() {
+	data := strings.Split(event.Mailbox, "@")
+	if len(data) == 2 {
+		event.Extension = data[0]
+	}
+}

+ 90 - 0
internal/app/ami/model/queue.go

@@ -0,0 +1,90 @@
+package model
+
+//QueueParams 队列参数
+type QueueParams struct {
+	Queue            string         `json:"queue"`
+	Max              string         `json:"max"`
+	Strategy         string         `json:"strategy"`
+	Calls            string         `json:"calls"`
+	Holdtime         string         `json:"holdtime"`
+	TalkTime         string         `json:"talkTime"`
+	Completed        string         `json:"completed"`
+	Abandoned        string         `json:"abandoned"`
+	ServiceLevel     string         `json:"serviceLevel"`
+	ServicelevelPerf string         `json:"servicelevelPerf"`
+	Weight           string         `json:"weight"`
+	Members          []*QueueMember `json:"members"`
+	Entrys           []*QueueEntry  `json:"entrys"`
+}
+
+//QueueMember 队列成员
+type QueueMember struct {
+	Queue          string `json:"queue"`
+	Name           string `json:"name"`
+	Location       string `json:"location"`
+	StateInterface string `json:"stateInterface"`
+	Membership     string `json:"membership"`
+	Penalty        string `json:"penalty"`
+	CallsTaken     string `json:"callsTaken"`
+	LastCall       string `json:"lastCall"`
+	InCall         string `json:"inCall"`
+	Status         string `json:"status"`
+	Paused         string `json:"paused"`
+	PausedReason   string `json:"pausedReason"`
+}
+
+type QueueEntry struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	Wait              string `json:"wait"`
+}
+
+type QueueCallerJoin struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	Count             string `json:"count"`
+}
+
+type QueueCallerAbandon struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	HoldTime          string `json:"holdTime"`
+}
+
+type QueueCallerLeave struct {
+	CallerIDName      string `json:"callerIDName"`
+	CallerIDNum       string `json:"callerIDNumber"`
+	Channel           string `json:"channel"`
+	ConnectedLineName string `json:"connectedLineName"`
+	ConnectedLineNum  string `json:"connectedLineNumber"`
+	Event             string `json:"event"`
+	Position          string `json:"position"`
+	Priority          string `json:"priority"`
+	Queue             string `json:"queue"`
+	Uniqueid          string `json:"uniqueid"`
+	Count             string `json:"count"`
+}

+ 71 - 0
internal/app/index.go

@@ -0,0 +1,71 @@
+package app
+
+import (
+	"os/exec"
+	"pbx-api-gin/internal/app/ami/action"
+	"pbx-api-gin/internal/app/stc"
+	"pbx-api-gin/internal/app/stc/active"
+	"pbx-api-gin/internal/app/stc/priority"
+	"pbx-api-gin/internal/app/stc/socket"
+	"pbx-api-gin/pkg/lfshook"
+	"pbx-api-gin/pkg/utils"
+	"syscall"
+	"time"
+)
+
+func StartApp() {
+
+	//init asterisk
+	if !utils.CheckAsterisk() {
+		lfshook.NewLogger().Infof("Check asterisk , if not running , run cmd service asterisk start !")
+		cmd := exec.Command("service", "asterisk", "start")
+		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+		err := cmd.Run()
+		if err != nil {
+			lfshook.NewLogger().Infof("Failed to start asterisk: %v", err)
+			return
+		}
+
+		time.Sleep(3 * time.Second)
+	}
+
+	//Get cab number acording to IP
+	socket.SetMasterCabNum()
+
+	utils.LoggerDebug.Printf("Get CabNumber:%s  Init Master:%+v", active.CabNum, active.Master)
+
+	//init the active status
+	//active.ActivedCab = ""
+
+	//get priority
+	priority.GetPriority()
+
+	//初始化列车信息map 列车号+车厢号
+	active.InitTrainInfoMap()
+
+	// 启动带有重连机制的连接管理协程MC1
+	go stc.StartStcConnection(socket.Conn, "1")
+
+	// 启动带有重连机制的连接管理协程MC8
+	go stc.StartStcConnection(socket.Conn8, "8")
+
+	//启动连接到Master服务器,检查Master是否在线
+	//if active.CabNum == "1" {
+	//	go stc.StartConnectionToSipServer(socket.ConnToSlave)
+	//} else {
+	//	go stc.RecvFromSipServer()
+	//}
+
+	// 启动其他服务...
+	// 启动 AMI
+	go func() {
+		action.StartAMI(func() {
+			utils.LoggerDebug.Printf("AMI callback Start .")
+		}, []func(event map[string]string){})
+	}()
+
+	//refresh extension status
+	time.Sleep(1 * time.Second)
+
+	utils.ExecCmdAsync("/usr/sbin/asterisk", "-rx", "reload")
+}

+ 157 - 0
internal/app/stc/active/index.go

@@ -0,0 +1,157 @@
+package active
+
+import (
+	"encoding/xml"
+	"fmt"
+	"net/http"
+	"os"
+	"pbx-api-gin/pkg/utils"
+	"time"
+)
+
+// var ActiveCab string
+var CabNum string
+var ActivedCab = "1"
+var PADTimeout = 30
+var Master = false
+
+var ActivedCabDelay = "1"
+
+var TrainDevide = 0
+var RadioFault1 = 0
+var RadioFault8 = 0
+
+var TrainInfoMap = make(map[string]map[int]string)
+
+// 初始化所有列车号
+var TrainNumGr = []string{"TS1", "TS2", "TS3", "TS4", "TS5", "TS6", "TS7", "TS8", "TS9", "TS10", "TS11", "TS12", "TS13"}
+var TrainNum = "" //STC 设置该变量,可对应取车厢号
+
+// 初始化所有车厢号
+var data = [][]string{
+	{"D433", "P433", "M433", "K433", "C434", "M434", "P434", "D434"},
+	{"D435", "P435", "M435", "K435", "C436", "M436", "P436", "D436"},
+	{"D437", "P437", "M437", "K437", "C438", "M438", "P438", "D438"},
+	{"D439", "P439", "M439", "K439", "C440", "M440", "P440", "D440"},
+	{"D441", "P441", "M441", "K441", "C442", "M442", "P442", "D442"},
+	{"D443", "P443", "M443", "K443", "C444", "M444", "P444", "D444"},
+	{"D445", "P445", "M445", "K445", "C446", "M446", "P446", "D446"},
+	{"D447", "P447", "M447", "K447", "C448", "M448", "P448", "D448"},
+	{"D449", "P449", "M449", "K449", "C450", "M450", "P450", "D450"},
+	{"D451", "P451", "M451", "K451", "C452", "M452", "P452", "D452"},
+	{"D453", "P653", "M453", "K453", "C454", "M454", "P454", "D454"},
+	{"D455", "P455", "M455", "K455", "C456", "M456", "P456", "D456"},
+	{"D457", "P457", "M457", "K457", "C458", "M458", "P458", "D458"},
+}
+
+// Pacu 表示单个 pacu 节点
+type Pacu struct {
+	ID     string `xml:"id"`
+	Status string `xml:"status"`
+	Volume string `xml:"volume"`
+	Mute   bool   `xml:"mute"`
+}
+
+// Icp 表示单个 icp 节点
+type Icp struct {
+	ID     string `xml:"id"`
+	Volume string `xml:"volume"`
+}
+
+type Device struct {
+	PacuInfo [8]Pacu `xml:"-"` // 不自动序列化,由自定义方法处理
+	ICPInfo  [2]Icp  `xml:"-"`
+}
+
+var DeviceEndpoint Device
+
+// var SetTimer = false
+var QueueTimer *time.Timer = nil
+
+// 挂断所有报警器
+func NotifyPaiu(Exten, Action string) {
+	url := ""
+	switch Action {
+	case "answer":
+		url = fmt.Sprintf("http://10.0.24.%s/api/sipphone?action=answer", Exten[2:])
+	case "hold":
+		url = fmt.Sprintf("http://10.0.24.%s/api/sipphone?action=hold", Exten[2:])
+	case "hangup":
+		url = fmt.Sprintf("http://10.0.24.%s/api/sipphone?action=hangup", Exten[2:])
+	}
+
+	utils.LoggerDebug.Printf("Notify PAD URL:%+v ", url)
+	resp, err := http.Get(url)
+	if err != nil {
+		//lfshook.NewLogger().Logger.Infof("======Notify PAIU Alarm====:%+v ", err)
+		return
+	}
+	defer resp.Body.Close()
+	/*
+	   body, err := io.ReadAll(resp.Body)
+
+	   	if err != nil {
+	   		// 读取数据错误
+	   		lfshook.NewLogger().Warn("ioutil ReadAll failed :", err.Error())
+	   		return
+	   	}
+
+	   fmt.Printf("状态码: %d\n", resp.StatusCode)
+	   fmt.Printf("响应内容: %s\n", body)
+	*/
+}
+
+// MarshalXML 实现自定义 XML 输出:按顺序写入所有 pacu 和 icp 节点
+func (d Device) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+	// 写入根标签 <device>
+	if err := e.EncodeToken(xml.StartElement{Name: xml.Name{Local: "device"}}); err != nil {
+		return err
+	}
+
+	// 写入所有 <pacu> 节点
+	for _, p := range DeviceEndpoint.PacuInfo {
+		if err := e.Encode(p); err != nil {
+			return err
+		}
+	}
+
+	// 写入所有 <icp> 节点
+	for _, i := range DeviceEndpoint.ICPInfo {
+		if err := e.Encode(i); err != nil {
+			return err
+		}
+	}
+
+	// 写入根结束标签 </device>
+	return e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "device"}})
+}
+
+func GenerateXML(pt string) error {
+	// 创建文件(会覆盖同名文件)
+	file, err := os.Create(pt)
+	if err != nil {
+		//fmt.Printf("创建文件失败: %v\n", err)
+		return err
+	}
+	defer file.Close() // 确保关闭
+
+	// 使用 xml.Encoder 写入(更规范,支持缩进、处理特殊字符等)
+	encoder := xml.NewEncoder(file)
+	encoder.Indent("", "  ") // 启用缩进,等效于 MarshalIndent
+	if err := encoder.Encode(DeviceEndpoint); err != nil {
+		//fmt.Printf("XML 编码写入失败: %v\n", err)
+		return err
+	}
+	return err
+}
+
+func InitTrainInfoMap() {
+	utils.LoggerDebug.Printf("Init train info map ..... ")
+
+	for i, train := range TrainNumGr {
+		TrainInfoMap[train] = make(map[int]string)
+		for j, car := range data[i] {
+			TrainInfoMap[train][j+1] = car //位置从1开始
+		}
+	}
+}

+ 898 - 0
internal/app/stc/broadcast/stc-broadcast.go

@@ -0,0 +1,898 @@
+package broadcast
+
+import (
+	"bytes"
+	"context"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"net"
+	"os/exec"
+	"pbx-api-gin/internal/app/ami/action"
+	"pbx-api-gin/internal/app/stc/active"
+	msgdata "pbx-api-gin/internal/app/stc/data"
+	"pbx-api-gin/internal/app/stc/priority"
+	alstatus "pbx-api-gin/internal/app/stc/sendstatus"
+	"pbx-api-gin/pkg/lfshook"
+	"pbx-api-gin/pkg/utils"
+	"strconv"
+	"sync"
+	"syscall"
+	"time"
+)
+
+var tagLog = 0
+
+func HandleStcCmd(ctx context.Context, conn net.Conn) {
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+
+		default:
+			var buf bytes.Buffer
+			tmp := make([]byte, 1024)
+
+			if conn != nil {
+				n, err := conn.Read(tmp)
+				if err != nil {
+					if err != io.EOF {
+						conn.Close()
+					}
+					return
+				}
+				buf.Write(tmp[:n])
+			}
+			//lfshook.NewLogger().Logger.Infof("buf:%x==============================", buf.Bytes())
+			for {
+				packet, err := msgdata.ExtractPacket(&buf)
+				if err != nil {
+					utils.LoggerDebug.Printf("Parse error: %v, resetting buffer", err)
+					buf.Reset() // 解析失败,清空避免污染
+					break
+				}
+				if packet == nil {
+					break // 当前无完整包,等待下次 ReadFrom
+				}
+				//处理 packet...
+				go processPacket(packet)
+			}
+
+		}
+	}
+}
+
+// get train info and pacu info from heartbeat
+func getInfofromSTC(info []byte) {
+	//dataCount := info[0]
+	//Get train info
+	value := binary.BigEndian.Uint16(info[2:4]) //info[2] +  info[3] 列车号数据
+	//value := 3
+	active.TrainNum = "TS" + strconv.Itoa(int(value))
+
+	lfshook.NewLogger().Logger.Infof("TrainNum: %s", active.TrainNum)
+
+	//Get ICP volume
+	icpBit8 := info[9] & 0xF
+	icpBit1 := (info[9] >> 4) & 0xF
+	icpVol1 := 0
+	icpVol8 := 0
+
+	if icpBit1 == 0xe {
+		icpVol1 = 100
+	} else {
+		icpVol1 = int(icpBit1) * 7
+	}
+
+	if icpBit8 == 0xe {
+		icpVol8 = 100
+	} else {
+		icpVol8 = int(icpBit8) * 7
+	}
+
+	active.DeviceEndpoint.ICPInfo[0].Volume = fmt.Sprintf("%d", icpVol1)
+	active.DeviceEndpoint.ICPInfo[0].ID = "1"
+	active.DeviceEndpoint.ICPInfo[1].Volume = fmt.Sprintf("%d", icpVol8)
+	active.DeviceEndpoint.ICPInfo[1].ID = "8"
+
+	//Get Pacu info
+	eidsStat := info[1] //PACU mute
+	pacuStat := info[4] //PACU status
+
+	pacuVolume := binary.BigEndian.Uint32(info[5:9]) //info[5]-info[8]
+
+	//lfshook.NewLogger().Logger.Infof("=====pacuVolume====info[5:9]=======%x", info[5:9])
+	//lfshook.NewLogger().Logger.Infof("=====pacuStatus====info[4]=======%x", info[4])
+	for i := 0; i < 8; i++ {
+		eidsBit := (eidsStat >> i) & 0x01 // 右移 i 位,再与 1 取最低位
+		//fmt.Printf("eidsBit bit %d = %d\n", i, eidsBit) // bit 0 是 LSB(最低位,最右)
+
+		//get 1 bit every time
+		pacuStatBit := (pacuStat >> i) & 0x01
+		//fmt.Printf("pacuStatBit bit %d = %d\n", i, pacuStatBit)
+
+		//get 4 bit every time
+		//var pacuVolBit byte
+		pacuVolBit := (pacuVolume >> (i * 4)) & 0x0000000F
+		//fmt.Printf("pacuVolBit bit %d = %x\n", i, pacuVolBit)
+
+		active.DeviceEndpoint.PacuInfo[i].ID = strconv.Itoa(i + 1)
+		if eidsBit == 1 {
+			active.DeviceEndpoint.PacuInfo[i].Mute = true
+		} else {
+			active.DeviceEndpoint.PacuInfo[i].Mute = false
+		}
+		if pacuStatBit == 0 {
+			active.DeviceEndpoint.PacuInfo[i].Status = "normal"
+		} else {
+			active.DeviceEndpoint.PacuInfo[i].Status = "abnormal"
+		}
+		if int(pacuVolBit) == 14 {
+			active.DeviceEndpoint.PacuInfo[i].Volume = "100"
+		} else {
+			active.DeviceEndpoint.PacuInfo[i].Volume = strconv.Itoa(int(pacuVolBit) * 7)
+		}
+		//lfshook.NewLogger().Logger.Infof("PACU INFO===ID:%s===Mute:%+v===Stat:%s===Vol:%s", active.DeviceEndpoint.PacuInfo[i].ID, active.DeviceEndpoint.PacuInfo[i].Mute, active.DeviceEndpoint.PacuInfo[i].Status, active.DeviceEndpoint.PacuInfo[i].Volume)
+	}
+}
+
+// 处理单个数据包(原 switch 逻辑迁移过来)
+func processPacket(packet []byte) {
+
+	if len(packet) < 6 {
+		utils.LoggerDebug.Printf("Get data wrong length from STC ! Data:%x", packet)
+		return
+	}
+
+	//for recv data log debug
+	if packet[5] != 0x03 && packet[5] != 0x0c && packet[5] != 0x01 {
+		utils.LoggerDebug.Printf("Get data from STC:%x", packet)
+	}
+
+	//check if the cmd type is avtive
+	if packet[5] == 0x03 { // ACTIVE
+		Active([3]byte{packet[8], packet[9], packet[10]})
+		return
+	}
+
+	//check if Master role
+	if !active.Master {
+		if tagLog == 0 {
+			utils.LoggerDebug.Printf("Not Master Role , Ignore all data from STC !")
+			tagLog = 1
+		}
+		return
+	}
+	tagLog = 0
+
+	switch packet[5] {
+	case 0x01: //heartbeat
+		return
+
+	case 0x02: // STN
+		if active.ActivedCab != "" {
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("STN")
+
+			if priority.CheckPriority("STN") {
+
+				runningTaskName := action.InterruptRunningTask("STN") //STN interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				StationAnn(packet)
+			} else {
+				if priority.TaskCreating == "STN" {
+					utils.LoggerDebug.Printf("STN : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "STN", "refuse")
+			}
+		}
+
+		time.Sleep(1 * time.Second)
+		if priority.TaskCreating == "STN" {
+			utils.LoggerDebug.Printf("STN : Clean priority.TaskCreating = '' !")
+			priority.TaskCreating = ""
+		}
+
+	case 0x05: // SPC
+		if active.ActivedCab != "" {
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("SPC")
+
+			if priority.CheckPriority("SPC") {
+
+				runningTaskName := action.InterruptRunningTask("SPC") //SPC interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				SpecialAnn(packet)
+			} else {
+				if priority.TaskCreating == "SPC" {
+					utils.LoggerDebug.Printf("SPC : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "SPC", "refuse")
+			}
+		}
+
+		time.Sleep(1 * time.Second)
+		if priority.TaskCreating == "SPC" {
+			utils.LoggerDebug.Printf("SPC : Clean priority.TaskCreating = '' !")
+			priority.TaskCreating = ""
+		}
+
+	case 0x06: // EMG
+
+		if active.ActivedCab != "" {
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("EMG")
+
+			if priority.CheckPriority("EMG") {
+
+				runningTaskName := action.InterruptRunningTask("EMG") //EMG interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				EmgMsg(packet)
+			} else {
+				if priority.TaskCreating == "EMG" {
+					utils.LoggerDebug.Printf("EMG : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "EMG", "refuse")
+			}
+		}
+
+		time.Sleep(1 * time.Second)
+		if priority.TaskCreating == "EMG" {
+			utils.LoggerDebug.Printf("EMG : Clean priority.TaskCreating = '' !")
+			priority.TaskCreating = ""
+		}
+
+	case 0x07: // STOP
+		AnnStop([4]byte{packet[8], packet[9], packet[10], packet[11]})
+
+	case 0x08: // DCS
+
+		if active.ActivedCab != "" {
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("DCS")
+
+			if priority.CheckPriority("DCS") {
+
+				runningTaskName := action.InterruptRunningTask("DCS") //DCS interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				DcsAnn(packet)
+			} else {
+				if priority.TaskCreating == "DCS" {
+					utils.LoggerDebug.Printf("DCS : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "DCS", "refuse")
+			}
+
+			time.Sleep(1 * time.Second)
+			if priority.TaskCreating == "DCS" {
+				utils.LoggerDebug.Printf("DCS : Clean priority.TaskCreating = '' !")
+				priority.TaskCreating = ""
+			}
+		}
+
+	case 0x09: // SELF CHECK
+
+		if active.ActivedCab != "" {
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("CHK")
+
+			if priority.CheckPriority("CHK") {
+
+				runningTaskName := action.InterruptRunningTask("CHK") //CHK interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				SelfCheck(packet)
+			} else {
+				if priority.TaskCreating == "CHK" {
+					utils.LoggerDebug.Printf("CHK : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "CHK", "refuse")
+			}
+		}
+
+		time.Sleep(1 * time.Second)
+		if priority.TaskCreating == "CHK" {
+			utils.LoggerDebug.Printf("CHK : Clean priority.TaskCreating = '' !")
+			priority.TaskCreating = ""
+		}
+
+	case 0x0a: // Tone-test
+		if active.ActivedCab != "" {
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("VOL")
+
+			if priority.CheckPriority("VOL") {
+
+				runningTaskName := action.InterruptRunningTask("VOL") //VOL interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				ToneTest(packet)
+			} else {
+				if priority.TaskCreating == "VOL" {
+					utils.LoggerDebug.Printf("VOL : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "VOL", "refuse")
+			}
+		}
+
+		time.Sleep(1 * time.Second)
+		if priority.TaskCreating == "VOL" {
+			utils.LoggerDebug.Printf("VOL : Clean priority.TaskCreating = '' !")
+			priority.TaskCreating = ""
+		}
+
+	case 0x0e: //TMS answer PAD
+
+		handler := packet[8]
+
+		//Drop other handler in 2 sec
+		//PACUs---call---->ICP1
+		//PAD---->Chanspy(WEq)-->ICP1;PAD--->Call---->ICP2
+		if handler == 0x01 { //answer PAD
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("PAD-TMS")
+
+			//if packet[8] == 0x01 { //answer PAD
+			if priority.CheckPriority("PAD-TMS") {
+
+				//Before Answer PAD
+				//if packet[8] == 0x01 {
+
+				runningTaskName := action.InterruptRunningTask("PAD-TMS") //PAD-TMS interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				//}
+
+				if active.QueueTimer != nil {
+					if active.QueueTimer.Stop() {
+						utils.LoggerDebug.Printf("Stop PAD timer true !")
+					} else {
+						utils.LoggerDebug.Printf("Stop PAD timer false !")
+					}
+				}
+
+				AlarmHandleTMS(packet)
+			} else {
+				if priority.TaskCreating == "PAD-TMS" {
+					utils.LoggerDebug.Printf("PAD-TMS : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "PAD-TMS", "refuse")
+			}
+		} else { //hangup + hold
+			AlarmHandleTMS(packet)
+		}
+
+		time.Sleep(1 * time.Second)
+		if priority.TaskCreating == "PAD-TMS" {
+			utils.LoggerDebug.Printf("PAD-TMS : Clean priority.TaskCreating = '' !")
+			priority.TaskCreating = ""
+		}
+
+	case 0x0b: // reset all PAD
+		AlarmHoldResetAll(packet[8]) // reset all pad
+
+	case 0x0c: // Set PAD timeout
+		//lfshook.NewLogger().Logger.Infof("==type PADTimeout 0x0c===Get data from STC ====%x", packet)
+		PadTimeOutSetting(packet[8:]) // timeout setting
+
+		getInfofromSTC(packet[8:])
+
+	case 0x0d: // ICP answer PAD
+
+		handler := packet[8]
+
+		//Drop other handler in 2 sec
+		//PACUs---call---->ICP1
+		//PAD---->Chanspy(WEq)-->ICP1;PAD--->Call---->ICP2
+		if handler == 0x01 {
+
+			//检查是否有任务正在创建
+			action.WaitTaskCreate("PAD-ICP")
+
+			//if packet[8] == 0x01 { //answer PAD
+			if priority.CheckPriority("PAD-ICP") {
+
+				//Before Answer PAD
+				//if packet[8] == 0x01 {
+
+				runningTaskName := action.InterruptRunningTask("PAD-ICP") //PAD-ICP interrupt other
+				if runningTaskName != "" {
+					time.Sleep(time.Millisecond * 100) //wait endpoint release
+				}
+				//}
+
+				if active.QueueTimer != nil {
+					utils.LoggerDebug.Printf("PAD Timeout timer != nil !")
+					if active.QueueTimer.Stop() {
+						utils.LoggerDebug.Printf("Stop PAD timer true !")
+					} else {
+						utils.LoggerDebug.Printf("Stop PAD timer false !")
+					}
+				}
+
+				AlarmHandleICP(packet)
+			} else {
+				if priority.TaskCreating == "PAD-ICP" {
+					utils.LoggerDebug.Printf("PAD-ICP : Clean priority.TaskCreating = '' !")
+					priority.TaskCreating = ""
+				}
+				alstatus.PaStatus("", "PAD-ICP", "refuse")
+			}
+		} else { //hangup + hold
+			AlarmHandleICP(packet)
+		}
+
+		time.Sleep(1 * time.Second)
+		if priority.TaskCreating == "PAD-ICP" {
+			utils.LoggerDebug.Printf("PAD-ICP : Clean priority.TaskCreating = '' !")
+			priority.TaskCreating = ""
+		}
+
+		/*case 0x11: //Set remote master
+		if packet[8] == 1 {
+			utils.LoggerDebug.Printf("TMS respond, Stop PAD timeout %d sec .", active.PADTimeout)
+			if active.QueueTimer != nil {
+				if active.QueueTimer.Stop() {
+					utils.LoggerDebug.Printf("Stop PAD timer true !")
+				} else {
+					utils.LoggerDebug.Printf("Stop PAD timer false !")
+				}
+			}
+		} else {
+			utils.LoggerDebug.Printf("TMS respond canceled, set PAD timeout %d sec .", active.PADTimeout)
+			active.QueueTimer = time.AfterFunc(time.Duration(active.PADTimeout)*time.Second, func() { // check the PAD 30s timeout
+
+				res, err := action.QueueStatus("0301", "") // check OCC queue , if empty OCC-PAD start
+				if err != nil {
+					lfshook.NewLogger().Infof("OCC QueueStatus err:%+v", err)
+					return
+				}
+				if res == nil {
+					return
+				}
+
+				if res.Calls == "0" { // OCC queue empty
+					priority.ICPAnswer = 0
+					for _, ret := range alstatus.PadQueues {
+						//order by pos
+						action.RedirectInQueue(ret.Exten, "0301", "queues-occ", ret.Exten) // redirect All ICP-PAD redirect to OCC queue
+						time.Sleep(time.Millisecond * 100)
+					}
+				}
+			})
+		}*/
+		//default:
+		//fmt.Printf("Unknown command: %x\n", packet[5])
+	}
+}
+
+func PadTimeOutSetting(data []byte) {
+
+	Seconds := data[0]
+	if Seconds != 0 {
+		active.PADTimeout = int(Seconds)
+		//lfshook.NewLogger().Logger.Infof("=========Set PAD Timeout seconds to %d ! ============", active.PADTimeout)
+	}
+
+}
+
+// STN , 自动报站广播
+func StationAnn(data []byte) (err error) {
+
+	specialVoice := int(data[8])
+	delay := data[9]
+	cycleCount := data[10]
+	datalen := int(data[11])
+
+	filename := msgdata.SubstrByRune(string(data[12:]), 0, datalen-4)
+
+	//set spc voice tag
+	priority.SpecialVoice = specialVoice
+
+	utils.LoggerDebug.Printf("Type:STN	FileName:%x	Count:%x	SpecialVoice:%+v	Interval:%+v", filename, cycleCount, specialVoice, delay)
+
+	action.PlaybackPacu(strconv.Quote(filename), int(cycleCount), int(delay), "STN")
+	return nil
+}
+
+// 激活信号
+func Active(data [3]byte) {
+
+	//var info model.Sysinfo
+	Signal := int(data[0])
+	master := int(data[1])
+	TrainInfo := int(data[2])
+
+	lfshook.NewLogger().Logger.Infof("=====active:%x======cab=%s Master=%d=====data:%x======ActivedCabDelay:%x", Signal, active.CabNum, master, data, active.ActivedCabDelay)
+
+	//Set Master
+	if master == 1 && active.CabNum == "1" {
+
+		active.Master = true
+		if !utils.CheckAsterisk() {
+			lfshook.NewLogger().Infof("Check asterisk , if not running , run cmd service asterisk start !")
+			cmd := exec.Command("service", "asterisk", "start")
+			cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+			err := cmd.Run()
+			if err != nil {
+				lfshook.NewLogger().Infof("Failed to start asterisk: %v", err)
+				return
+			}
+		}
+	} else if master == 8 && active.CabNum == "8" {
+
+		active.Master = true
+		if !utils.CheckAsterisk() {
+			lfshook.NewLogger().Infof("Check asterisk , if not running , run cmd service asterisk start !")
+			cmd := exec.Command("service", "asterisk", "start")
+			cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+			err := cmd.Run()
+			if err != nil {
+				lfshook.NewLogger().Infof("Failed to start asterisk: %v", err)
+				return
+			}
+		}
+	} else if master == 8 && active.CabNum == "1" {
+
+		if utils.CheckAsterisk() {
+			lfshook.NewLogger().Infof("Check asterisk , if running slave, run cmd service asterisk stop !")
+			cmd := exec.Command("service", "asterisk", "stop")
+			cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+			err := cmd.Run()
+			if err != nil {
+				lfshook.NewLogger().Infof("Failed to stop asterisk: %v", err)
+				return
+			}
+		}
+	} else if master == 1 && active.CabNum == "8" {
+		active.Master = false
+	}
+
+	//Set train info
+	if TrainInfo != 0 {
+		//Get train devide info
+		DevideInfo := TrainInfo & 0x20
+		if DevideInfo == 0x20 {
+			active.TrainDevide = 1
+
+			active.Master = true //列车断开,设置两边都Master
+			if !utils.CheckAsterisk() {
+				lfshook.NewLogger().Infof("Check asterisk , if not running , run cmd service asterisk start !")
+				cmd := exec.Command("service", "asterisk", "start")
+				cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+				err := cmd.Run()
+				if err != nil {
+					lfshook.NewLogger().Infof("Failed to start asterisk: %v", err)
+					return
+				}
+			}
+		} else {
+			active.TrainDevide = 0
+		}
+
+		//Radio fault
+		active.RadioFault1 = TrainInfo & 0x03
+		active.RadioFault8 = TrainInfo & 0x0c
+		//if RadioFault1 == 2 || RadioFault8 == 2 {
+		//	active.RadioFault = 1
+		//} else {
+		//	active.RadioFault = 0
+		//}
+		//utils.LoggerDebug.Printf("RadioFault1:%x 	RadioFault8:%x		DevideInfo:%x", active.RadioFault1, active.RadioFault8, DevideInfo)
+
+	}
+
+	switch Signal {
+	case 0:
+
+		if active.ActivedCab != "" {
+			active.ActivedCab = ""
+			action.InActiveHangup()
+		}
+
+	case 1:
+		//active signal from 8 to 1
+		if active.ActivedCab == "8" || active.ActivedCab == "" {
+			active.ActivedCab = "1"
+			active.ActivedCabDelay = "1"
+
+			action.InActiveHangup()
+		}
+
+	case 8:
+		//active signal from 1 to 8
+		if active.ActivedCab == "1" || active.ActivedCab == "" {
+			active.ActivedCab = "8"
+			active.ActivedCabDelay = "8"
+
+			action.InActiveHangup()
+		}
+
+	}
+}
+
+// SPC ,特殊服务消息广播
+func SpecialAnn(data []byte) {
+
+	delay := data[8]
+	cycleCount := data[9]
+	datalen := int(data[10])
+
+	filename := msgdata.SubstrByRune(string(data[11:]), 0, datalen-4)
+
+	utils.LoggerDebug.Printf("Type:SPC	FileName:%x	Count:%x	Interval:%+v", filename, cycleCount, delay)
+	if int(cycleCount) == 255 {
+		action.PlaybackPacu(strconv.Quote(filename), 9999999, int(delay), "SPC")
+	} else {
+		action.PlaybackPacu(strconv.Quote(filename), int(cycleCount), int(delay), "SPC")
+	}
+}
+
+// EMG ,紧急服务消息广播
+func EmgMsg(data []byte) {
+	delay := data[8]
+	cycleCount := data[9]
+	datalen := int(data[10])
+
+	filename := msgdata.SubstrByRune(string(data[11:]), 0, datalen-4)
+
+	utils.LoggerDebug.Printf("Type:EMG	FileName:%x	Count:%x	Interval:%+v", filename, cycleCount, delay)
+	if int(cycleCount) == 255 {
+		action.PlaybackPacu(strconv.Quote(filename), 9999999, int(delay), "EMG")
+	} else {
+		action.PlaybackPacu(strconv.Quote(filename), int(cycleCount), int(delay), "EMG")
+	}
+}
+
+// 停止指定类型广播
+func AnnStop(data [4]byte) {
+
+	//lfshook.NewLogger().Logger.Infof("=========AnnStop Type  %x", data[0])
+	utils.LoggerDebug.Printf("Stop PA Type:%x (DCS=3,EMG=4,SPC=7,STN=8,SelfCheck=9,ToneTest=10)", data[0])
+
+	switch data[0] {
+	case 0x03:
+
+		action.HangupTask("DCS")           //STOP DCS
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+	case 0x04:
+
+		action.HangupTask("EMG")           //STOP EMG
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+	case 0x07:
+
+		action.HangupTask("SPC")           //STOP SPC
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+	case 0x08:
+
+		action.HangupTask("STN")           //STOP STN
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+	case 0x09:
+
+		action.HangupTask("CHK")           //STOP CHK
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+	case 0x0a:
+
+		action.HangupTask("VOL")           //STOP VOL
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+	default:
+		action.InterruptRunningTask("")
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+	}
+}
+
+// DCS 语音
+func DcsAnn(data []byte) {
+	delay := data[8]
+	cycleCount := data[9]
+	datalen := int(data[10])
+
+	filename := msgdata.SubstrByRune(string(data[11:]), 0, datalen-4)
+
+	utils.LoggerDebug.Printf("Type:DCS	FileName:%x	Count:%x	Interval:%+v", filename, cycleCount, delay)
+	if int(cycleCount) == 255 {
+		action.PlaybackPacu(strconv.Quote(filename), 9999999, int(delay), "DCS")
+	} else {
+		action.PlaybackPacu(strconv.Quote(filename), int(cycleCount), int(delay), "DCS")
+	}
+}
+
+// tone-test广播
+func ToneTest(data []byte) {
+
+	check := data[8]
+	delay := data[9]
+	cycleCount := data[10]
+	datalen := int(data[11])
+
+	filename := msgdata.SubstrByRune(string(data[12:]), 0, datalen-4)
+
+	utils.LoggerDebug.Printf("Type:ToneTest	FileName:%x	 Count:%x	Interval:%+v	Action:%x (0x01=start/0x02=stop)", filename, cycleCount, delay, check)
+	switch check {
+	case 0x01: //start
+		action.PlaybackPacu(strconv.Quote(filename), int(cycleCount), int(delay), "VOL")
+
+	case 0x02: //stop
+		action.HangupAllExcept("")
+	}
+}
+
+// 自检广播
+func SelfCheck(data []byte) {
+
+	check := data[8]
+	delay := data[9]
+	cycleCount := data[10]
+	//cycleCount := 0x32
+	datalen := int(data[11])
+
+	filename := msgdata.SubstrByRune(string(data[12:]), 0, datalen-4)
+
+	utils.LoggerDebug.Printf("Type:SelfCehck	FileName:%x	 Count:%x	Interval:%+v	Action:%x (0x01=start/0x02=stop)", filename, cycleCount, delay, check)
+	switch check {
+	case 0x01: //start
+
+		action.PlaybackPacu(strconv.Quote(filename), int(cycleCount), int(delay), "CHK")
+	case 0x02: //stop
+		action.HangupAllExcept("")
+	}
+}
+
+// 全局变量:记录正在抑制的 exten
+var (
+	suppressedExts = sync.Map{} // map[string]struct{},值存在即表示被抑制
+	//suppressionMu  sync.Mutex   // 保护初始化和清理操作(可选)
+)
+
+// suppressKey 生成用于抑制的 key(可以根据需求扩展)
+func suppressKey(exten string, handler byte) string {
+	return fmt.Sprintf("%s_h%x", exten, handler)
+}
+
+// ICP操作乘客报警(根据激活信息判断转到1车还是8车================)
+func AlarmHandleICP(data []byte) {
+	handler := data[8]
+	carr := data[12]
+	pos := data[13]
+	exten := fmt.Sprintf("24%c%c", carr, pos)
+
+	utils.LoggerDebug.Printf("ICP Handle PAD:%s	Action:%x (answer=1,hold=2,hangup=3)", exten, handler)
+	switch handler {
+	case 0x01: //answer(ICP+Alarm+PACU)
+		//NotifyPaiu(exten, "answer")
+		priority.ICPAnswer = 1
+		//if priority.PADStart == 0 {
+		//alstatus.PaStatus("", "PAD", "start")
+		priority.InterruptedPad = ""
+		//	priority.PADStart = 1
+		//}
+		utils.LoggerDebug.Printf("ICP Answer PAD:%s .", exten)
+
+		if active.ActivedCab == "1" {
+			action.Dial("0402", "0511", "pad-rule-pacus", "ani1", exten, "1") // PACUs dial ICP1
+			//goto ami event ConfbridgeJoin, ICP answer PAD
+		} else if active.ActivedCab == "8" {
+			action.Dial("0402", "0511", "pad-rule-pacus", "ani8", exten, "8") // PACUs dial ICP8
+			//goto ami event ConfbridgeJoin, ICP answer PAD
+		} else if active.ActivedCab == "" { // No cab occupied
+			if active.ActivedCabDelay == "1" {
+				action.Dial("0402", "0511", "pad-rule-pacus", "ani1", exten, "1") // PACUs dial ICP1
+			} else if active.ActivedCabDelay == "8" {
+				action.Dial("0402", "0511", "pad-rule-pacus", "ani8", exten, "8") // PACUs dial ICP8
+			} else {
+				action.Dial("0402", "0511", "pad-rule-pacus", "ani1", exten, "1") // PACUs dial ICP1
+			}
+		}
+
+	case 0x02: //hold  重新放回队列里面
+		utils.LoggerDebug.Printf("ICP Hold PAD-ICP PAD:%s !", exten)
+		active.NotifyPaiu(exten, "hold")
+		err := action.RedirectInQueue(exten, "0300", "queues-icp-redirect", "1")
+		if err != nil {
+			utils.LoggerDebug.Printf("RedirectInQueue err:%+v", err)
+		}
+		action.InterruptRunningTask("")
+		time.Sleep(time.Millisecond * 100) //wait endpoint release
+		//action.HangupICP()
+
+	case 0x03: //hangup
+		//NotifyPaiu(exten, "hangup")
+		utils.LoggerDebug.Printf("STC Hangup PAD-ICP !")
+		action.Hangup(exten) //Pad
+		//action.HangupICP()
+
+		action.HangupTask("PAD-ICP")
+
+	}
+}
+
+// TMS操作乘客报警(根据激活信息判断转到1车还是8车================)
+func AlarmHandleTMS(data []byte) {
+	handler := data[8]
+	//extlen := data[9]
+	carr := data[12]
+	pos := data[13]
+	exten := fmt.Sprintf("24%c%c", carr, pos)
+	PacuNum := fmt.Sprintf("21%c1", carr)
+
+	utils.LoggerDebug.Printf("TMS Handle PAD:%s	PACU:%s	Action:%x (answer=1,hold=2,hangup=3)", exten, PacuNum, handler)
+
+	switch handler {
+	case 0x01: //answer(ICP+Alarm+PACU)
+		//PACU---call---->ICP1
+		//PAD---->Chanspy(WEq)-->ICP1;PAD--->Call---->ICP2
+		//if priority.PADStart == 0 {
+		//alstatus.PaStatus("", "PAD", "start")
+		priority.InterruptedPad = ""
+		//priority.PADStart = 1
+		//}
+		priority.ICPAnswer = 1
+		utils.LoggerDebug.Printf("TMS Answer PAD:%s 	PACU:%s", exten, PacuNum)
+
+		if action.ExtenStatus(PacuNum) == "Idle" {
+			if active.ActivedCab == "1" {
+				action.Dial("0403", PacuNum, "pad-tms-dial-pacu", PacuNum, exten, "1") // PACU dial ICP1
+				//goto ami event BridgeEnter, ICP8 whisper ICP1
+			} else if active.ActivedCab == "8" {
+				action.Dial("0403", PacuNum, "pad-tms-dial-pacu", PacuNum, exten, "8") // PACU dial ICP8
+				//goto ami event BridgeEnter, ICP1 whisper ICP8
+			} else if active.ActivedCab == "" { // No cab occupied
+				if active.ActivedCabDelay == "1" {
+					action.Dial("0403", PacuNum, "pad-tms-dial-pacu", PacuNum, exten, "1") // PACU dial ICP1
+				} else if active.ActivedCabDelay == "8" {
+					action.Dial("0403", PacuNum, "pad-tms-dial-pacu", PacuNum, exten, "8") // PACU dial ICP8
+				} else {
+					action.Dial("0403", PacuNum, "pad-tms-dial-pacu", PacuNum, exten, "1") // PACU dial ICP1
+				}
+			}
+		} else {
+			action.RedirectInQueue(exten, "0405", "default", exten) // PAD dial ICPs
+		}
+
+	case 0x02: //hold  重新放回队列里面
+		utils.LoggerDebug.Printf("ICP Hold PAD-TMS PAD:%s !", exten)
+		active.NotifyPaiu(exten, "hold")
+		err := action.RedirectInQueue(exten, "0300", "queues-icp-redirect", "1")
+		if err != nil {
+			utils.LoggerDebug.Printf("RedirectInQueue err:%+v", err)
+		}
+		action.HangupAllLocalChan()
+
+	case 0x03: //hangup
+		//NotifyPaiu(exten, "hangup")
+		utils.LoggerDebug.Printf("STC Hangup PAD-TMS !")
+		action.Hangup(exten) //Pad
+		action.HangupTask("PAD-TMS")
+		//action.HangupTask("PAD-ICP")
+	}
+}
+
+// 挂断所有报警器
+func AlarmHoldResetAll(handler byte) {
+	utils.LoggerDebug.Printf("Alarm Hold/Reset All !")
+	//hangup all actived PAD
+	action.HangupAllPAD()
+
+	//hangup running task
+	action.InterruptRunningTask("AlarmHoldResetAll") //Reset PAD ALL
+	time.Sleep(time.Millisecond * 100)               //wait endpoint release
+}

+ 202 - 0
internal/app/stc/data/msgdata.go

@@ -0,0 +1,202 @@
+package msgdata
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"pbx-api-gin/pkg/utils"
+)
+
+// Protocol 定义协议数据结构
+type Protocol struct {
+	StartBytes    [3]byte // 协议开始符
+	SourceID      uint8   // 源设备号
+	DestinationID uint8   // 目的设备号
+	MessageID     uint8   // 消息号
+	DataLength    uint16  // 数据长度
+	Data          []byte  // 数据
+	Checksum      uint8   // 异或校验码
+	EndByte       uint8   // 协议结束符
+}
+
+// NewProtocol 创建一个新的 Protocol 实例
+func NewProtocol() *Protocol {
+	return &Protocol{
+		StartBytes: [3]byte{0x7F, 0x8E, 0x9D},
+		EndByte:    0xFE,
+	}
+
+}
+
+// Encode 将 Protocol 结构体编码为字节切片
+func (p *Protocol) Encode() ([]byte, error) {
+	// 初始化字节缓冲区
+	var buf bytes.Buffer // 写入协议开始符
+	buf.Write(p.StartBytes[:])
+
+	//init src and dst ID
+	p.SourceID = 0x02
+	p.DestinationID = 0x01
+	// 写入源设备号、目的设备号和消息号
+	binary.Write(&buf, binary.BigEndian, p.SourceID)
+	binary.Write(&buf, binary.BigEndian, p.DestinationID)
+	binary.Write(&buf, binary.BigEndian, p.MessageID)
+
+	// 写入数据长度
+	binary.Write(&buf, binary.BigEndian, p.DataLength)
+
+	// 写入数据
+	buf.Write(p.Data[:])
+
+	// 计算校验码
+	checksum := p.CalculateChecksum()
+	p.Checksum = checksum // 写入校验码
+	binary.Write(&buf, binary.BigEndian, p.Checksum)
+
+	// 写入协议结束符
+	binary.Write(&buf, binary.BigEndian, p.EndByte)
+
+	return buf.Bytes(), nil
+}
+
+// CalculateChecksum 计算校验码
+func (p *Protocol) CalculateChecksum() uint8 {
+	// 初始化校验码
+	checksum := uint8(0)
+
+	// 跳过协议开始符
+	data := append([]byte{byte(p.SourceID), byte(p.DestinationID), byte(p.MessageID)},
+		append([]byte{byte(p.DataLength >> 8), byte(p.DataLength)}, p.Data...)...)
+
+	// 计算校验码
+	for _, b := range data {
+		checksum ^= b
+	}
+
+	return checksum
+}
+
+// Decode 解码字节切片为 Protocol 结构体
+func Decode(data []byte) (*Protocol, error) {
+	if len(data) < 10 { // 最小长度:3(StartBytes) +1(SourceID) +1(DestinationID) +1(MessageID) +2(DataLength) +1(Checksum) +1(EndByte)
+		return nil, fmt.Errorf("data too short")
+	}
+
+	p := NewProtocol()
+
+	// 读取协议开始符
+	copy(p.StartBytes[:], data[:3])
+
+	// 读取源设备号、目的设备号和消息号
+	p.SourceID = data[3]
+	p.DestinationID = data[4]
+	p.MessageID = data[5]
+
+	// 读取数据长度
+	p.DataLength = binary.BigEndian.Uint16(data[6:8])
+
+	// 读取数据
+	dataLength := int(p.DataLength)
+	if len(data) < 10+dataLength {
+		return nil, fmt.Errorf("data length mismatch")
+	}
+	p.Data = data[8 : 8+dataLength]
+
+	// 读取校验码
+	p.Checksum = data[8+dataLength]
+
+	// 读取协议结束符
+	p.EndByte = data[9+dataLength]
+
+	// 验证校验码
+	if p.Checksum != p.CalculateChecksum() {
+		return nil, fmt.Errorf("checksum mismatch")
+	}
+
+	return p, nil
+}
+func SubstrByRune(s string, start, length int) string {
+	runes := []rune(s)
+	if start >= len(runes) {
+		return ""
+	}
+	end := start + length
+	if end > len(runes) {
+		end = len(runes)
+	}
+	return string(runes[start:end])
+}
+
+func ExtractPacket(buf *bytes.Buffer) ([]byte, error) {
+	data := buf.Bytes()
+	leng := buf.Len()
+
+	//lfshook.NewLogger().Logger.Infof("========start=========ExtractPacket raw data len=%d, hex=%x", leng, data)
+
+	// Step 1: 查找起始标记 0x7f 0x8e 0x9d
+	startIdx := -1
+	for i := 0; i <= len(data)-3; i++ { // <= len-3 防止越界
+		if data[i] == 0x7f && data[i+1] == 0x8e && data[i+2] == 0x9d {
+			startIdx = i
+			break
+		}
+	}
+
+	if startIdx == -1 {
+		//无起始标记:保守策略——丢弃过长脏数据,保留最后 MAX_LOOKAHEAD 字节防漏包
+		const MAX_LOOKAHEAD = 32
+		if leng > MAX_LOOKAHEAD {
+			buf.Next(leng - MAX_LOOKAHEAD)
+			utils.LoggerDebug.Printf("No start marker found, trimmed buffer to last %d bytes .", MAX_LOOKAHEAD)
+		}
+		return nil, nil // 明确告知:需继续读取
+	}
+
+	// Step 2: 安全读取 DataLength(偏移 startIdx+6 ~ startIdx+8)
+	if startIdx+8 > len(data) {
+		utils.LoggerDebug.Printf("Data Error: startIdx=%d, need data[%d:%d] for DataLength but len=%d → insufficient data .",
+			startIdx, startIdx+6, startIdx+8, len(data))
+		return nil, nil // 数据不够,等下次读取
+	}
+
+	dataLen := binary.BigEndian.Uint16(data[startIdx+6 : startIdx+8])
+	//lfshook.NewLogger().Logger.Infof("ExtractPacket datalen=%d (0x%04x) at offset %d", dataLen, dataLen, startIdx+6)
+
+	// Step 3: 计算理论包结束位置:startIdx + 8(头长) + dataLen + 1(0xFE)
+	endPosTheo := startIdx + 8 + int(dataLen) + 1
+	if endPosTheo > len(data) {
+		utils.LoggerDebug.Printf("Data Error: theoretical endPos=%d > data len=%d → need more data .", endPosTheo, len(data))
+		return nil, nil // 包未收全,等下次读取
+	}
+	//lfshook.NewLogger().Logger.Infof("t=================end================%x", data[endPosTheo])
+	// Step 4: 在 [startIdx+8+dataLen, endPosTheo] 范围内查找 0xFE(容错:允许在理论位置附近 1~2 字节浮动)
+	searchStart := startIdx + 8 + int(dataLen)
+	searchEnd := endPosTheo
+	if searchEnd > len(data) {
+		searchEnd = len(data) // 安全截断
+	}
+
+	endIdx := -1
+	for i := searchStart; i <= searchEnd; i++ {
+		if data[i] == 0xFE {
+			endIdx = i
+			break
+		}
+	}
+
+	if endIdx == -1 {
+		utils.LoggerDebug.Printf("Data Error: End tag 0xFE not found in expected range [%d,%d), dataLen=%d .", searchStart, searchEnd, dataLen)
+		return nil, nil // 结束符缺失,但可能是延迟到达,继续等
+	}
+
+	//找到完整包:[startIdx, endIdx](含两端)
+	packetLen := endIdx - startIdx + 1
+	packet := make([]byte, packetLen)
+	copy(packet, data[startIdx:endIdx+1])
+
+	// 从 buffer 中移除已处理部分
+	buf.Next(endIdx + 1)
+	//lfshook.NewLogger().Logger.Infof("extracted packet len=%d, start=0x7f8e9d, end=0xFE", packetLen)
+	//lfshook.NewLogger().Logger.Infof("=============return============== %x", packet)
+	return packet, nil
+}

+ 438 - 0
internal/app/stc/index.go

@@ -0,0 +1,438 @@
+package stc
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"pbx-api-gin/internal/app/stc/active"
+	"pbx-api-gin/internal/app/stc/broadcast"
+	msgdata "pbx-api-gin/internal/app/stc/data"
+	"pbx-api-gin/internal/app/stc/socket"
+	"pbx-api-gin/pkg/utils"
+	"sync"
+	"syscall"
+
+	"time"
+)
+
+func StartStcConnection(conn net.Conn, cab string) {
+	utils.LoggerDebug.Printf("Starting Connect to STC%s ...", cab)
+
+	var connMux sync.Mutex // 保护 conn 的读写
+	var conn1 net.Conn
+	var err error
+	var logTag = 0
+
+	for {
+		// 尝试建立连接MC
+		conn1, err = CreateConnection(cab)
+		if err != nil || conn1 == nil {
+			time.Sleep(2 * time.Second)
+			utils.LoggerDebug.Printf("Reconnecting To Cab%s ......", cab)
+			continue
+		}
+
+		//set connection log
+		trainInfo := fmt.Sprintf("CabNumber %s", active.ActivedCab)
+		if logTag == 0 {
+			utils.Logger.Printf("Train Information: %s, Message: Connection to Cab%s STC is up !", trainInfo, cab)
+			utils.LoggerDebug.Printf("Connection to Cab%s STC1 is up !", cab)
+			logTag = 1
+		}
+
+		connMux.Lock()
+		oldConn := conn
+		if cab == "1" {
+			socket.Conn = conn1
+		} else {
+			socket.Conn8 = conn1
+		}
+		connMux.Unlock()
+
+		// 关闭旧连接(如果存在)
+		if oldConn != nil {
+			oldConn.Close()
+			//lfshook.NewLogger().Logger.Infof("Closed previous connection")
+		}
+
+		// 使用 context 控制所有协程的生命周期
+		ctx, cancel := context.WithCancel(context.Background())
+
+		// 启动消息处理MC1
+		go func() {
+			defer func() {
+				cancel() // 一旦任一协程退出,取消所有
+			}()
+			broadcast.HandleStcCmd(ctx, conn1) // 改造 HandleStcCmd 接收 ctx
+		}()
+
+		// 启动心跳MC1
+		go func() {
+			defer func() {
+				cancel()
+			}()
+			Sendheartbeat(ctx, conn1) // 改造 Sendheartbeat 接收 ctx
+		}()
+
+		// 等待连接断开(监听连接状态)
+		<-ctx.Done()
+
+		// 连接已断开,清理
+		cancel() // 确保所有 cancel 被调用
+		conn1.Close()
+
+		//set connection log
+		if logTag == 1 {
+			utils.Logger.Printf("Train Information: %s, Message: Connection to Cab%s STC is down !", trainInfo, cab)
+			logTag = 0
+		}
+
+		//lfshook.NewLogger().Logger.Info("Reconnecting in 1 second...")
+		time.Sleep(time.Second) // 重连前等待
+	}
+}
+
+// 返回错误而不是终止程序
+func CreateConnection(RemoteCab string) (net.Conn, error) {
+
+	if RemoteCab == "1" { // connect to MC1
+		//lfshook.NewLogger().Logger.Infof("========Connect Server MC1 IP:%s  :Port:%d", socket.RemoteAddr, socket.RemotePort)
+		// 创建 Dialer
+		if active.CabNum == "1" { //in cab1
+			dialer := &net.Dialer{
+				LocalAddr: &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: socket.LocalPort}, // 固定本地端口
+				Control:   controlTCPConn,
+				Timeout:   5 * time.Second,
+			}
+
+			DialAddr := fmt.Sprintf("%s:%d", socket.RemoteAddr, socket.RemotePort)
+			conn, err := dialer.Dial("tcp", DialAddr)
+			if err != nil {
+				//lfshook.NewLogger().Logger.Infof("========Connect server err :%+v", err)
+				return nil, err
+			}
+			utils.LoggerDebug.Printf("Connect success STC1:%s:%d from Cab1 !", socket.RemoteAddr, socket.RemotePort)
+			return conn, nil
+		} else { //in cab 8
+			dialer := &net.Dialer{
+				LocalAddr: &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: socket.LocalPort8}, // 固定本地端口
+				Control:   controlTCPConn,
+				Timeout:   5 * time.Second,
+			}
+
+			DialAddr := fmt.Sprintf("%s:%d", socket.RemoteAddr, socket.RemotePort)
+			conn, err := dialer.Dial("tcp", DialAddr)
+			if err != nil {
+				//lfshook.NewLogger().Logger.Infof("========Connect server err :%+v", err)
+				return nil, err
+			}
+			utils.LoggerDebug.Printf("Connect success STC1:%s:%d from Cab8 !", socket.RemoteAddr, socket.RemotePort)
+			return conn, nil
+		}
+	} else { // connect to MC8
+		//lfshook.NewLogger().Logger.Infof("========Connect server MC8 IP:%s  :Port:%d", socket.RemoteAddr8, socket.RemotePort)
+		// 创建 Dialer
+		if active.CabNum == "1" { //in cab1
+			dialer := &net.Dialer{
+				LocalAddr: &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: socket.LocalPort}, // 固定本地端口
+				Control:   controlTCPConn,
+				Timeout:   5 * time.Second,
+			}
+
+			DialAddr := fmt.Sprintf("%s:%d", socket.RemoteAddr8, socket.RemotePort)
+			conn, err := dialer.Dial("tcp", DialAddr)
+			if err != nil {
+				//lfshook.NewLogger().Logger.Infof("========Connect server err :%+v", err)
+				return nil, err
+			}
+			utils.LoggerDebug.Printf("Connect success STC8:%s:%d from Cab1 !", socket.RemoteAddr8, socket.RemotePort)
+			return conn, nil
+		} else { //in cab 8
+			dialer := &net.Dialer{
+				LocalAddr: &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: socket.LocalPort8}, // 固定本地端口
+				Control:   controlTCPConn,
+				Timeout:   5 * time.Second,
+			}
+
+			DialAddr := fmt.Sprintf("%s:%d", socket.RemoteAddr8, socket.RemotePort)
+			conn, err := dialer.Dial("tcp", DialAddr)
+			if err != nil {
+				//lfshook.NewLogger().Logger.Infof("========Connect server err :%+v", err)
+				return nil, err
+			}
+			utils.LoggerDebug.Printf("Connect success STC8:%s:%d from Cab8 !", socket.RemoteAddr8, socket.RemotePort)
+			return conn, nil
+		}
+	}
+}
+
+/*
+func StartConnectionToSipServer(conn net.Conn) {
+
+	lfshook.NewLogger().Infof("Connect to Slave Sip Server ... ")
+	var connMux sync.Mutex // 保护 conn 的读写
+	var conn1 net.Conn
+	var err error
+
+	for {
+		// 尝试建立连接MC
+		conn1, err = CreateConnectionSipServer()
+		if err != nil || conn1 == nil {
+			time.Sleep(2 * time.Second)
+			//lfshook.NewLogger().Logger.Infof("===========Reconnecting==Sip Server=======")
+			continue
+		}
+
+		connMux.Lock()
+		oldConn := conn
+
+		socket.ConnToSlave = conn1
+
+		connMux.Unlock()
+
+		// 关闭旧连接(如果存在)
+		if oldConn != nil {
+			oldConn.Close()
+			lfshook.NewLogger().Logger.Infof("Closed previous connection")
+		}
+
+		// 使用 context 控制所有协程的生命周期
+		ctx, cancel := context.WithCancel(context.Background())
+
+		// 启动心跳MC1
+		go func() {
+			defer func() {
+				cancel()
+			}()
+			SendToRemoteMaster(ctx, conn1) // 改造 Sendheartbeat 接收 ctx
+		}()
+
+		// 等待连接断开(监听连接状态)
+		<-ctx.Done()
+
+		// 连接已断开,清理
+		cancel() // 确保所有 cancel 被调用
+		conn1.Close()
+
+		lfshook.NewLogger().Logger.Info("Reconnecting in 1 second...")
+		time.Sleep(time.Second) // 重连前等待
+	}
+}
+*/
+/*
+// 连接Master sipserver
+func CreateConnectionSipServer() (net.Conn, error) {
+	dialer := &net.Dialer{
+		LocalAddr: &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: socket.LocalServerPort}, // 固定本地端口
+		Control:   controlTCPConn,
+		Timeout:   5 * time.Second,
+	}
+
+	DialAddr := fmt.Sprintf("%s:%d", socket.RemoteAddr8, socket.LocalServerPort) // Connect to Cab8 Sip server
+	conn, err := dialer.Dial("tcp", DialAddr)
+	if err != nil {
+		//lfshook.NewLogger().Logger.Infof("========Connect SIP server err :%+v", err)
+		return nil, err
+	}
+	lfshook.NewLogger().Logger.Infof("Connect SIP Server success :%s:%d", socket.RemoteAddr, socket.LocalPort)
+	return conn, nil
+}*/
+
+func controlTCPConn(network, address string, c syscall.RawConn) error {
+	return c.Control(func(fd uintptr) {
+		syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
+		// 注意:SO_REUSEPORT 在某些系统可用(如 Linux),但非标准
+	})
+}
+
+func Sendheartbeat(ctx context.Context, conn net.Conn) {
+	var count uint8
+
+	protocol := msgdata.NewProtocol()
+	protocol.MessageID = 0x21
+	protocol.DataLength = 0x04
+	protocol.Data = make([]byte, 4)
+
+	utils.LoggerDebug.Printf("Start Sendheartbeat to STC .")
+	// 初始化协议...
+	ticker := time.NewTicker(2 * time.Second)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			//lfshook.NewLogger().Logger.Infof("Sendheartbeat===ctx==ret======")
+			return
+
+		case <-ticker.C:
+			count++
+			protocol.Data[0] = count
+			// 编码并发送数据...
+			encoded, err := protocol.Encode()
+			if err != nil {
+				//fmt.Printf("encode err : %v\n", err)
+				return
+			}
+			if conn != nil {
+				_, err = conn.Write(encoded)
+				if err != nil {
+					//fmt.Printf("Send hearbeat err: %v\n", err)
+					conn.Close()
+					return // 触发重连
+				}
+				//lfshook.NewLogger().Logger.Infof("Sendheartbeat===send ======%x", encoded)
+			}
+		}
+	}
+}
+
+/*
+// cab == 8
+func RecvFromSipServer() {
+
+	lfshook.NewLogger().Infof("Connect to Master Sip Server  .... ")
+
+	listener, err := net.Listen("tcp", "0.0.0.0:10000")
+	if err != nil {
+		lfshook.NewLogger().Logger.Infof("Sever Listen cab1 err:%+v", err)
+	}
+	defer listener.Close()
+
+	for {
+		conn, err := listener.Accept() //blocked wait connection
+		if err != nil {
+			lfshook.NewLogger().Logger.Infof("Sever accept cab1 err:%+v", err)
+			continue
+		}
+
+		// 启动 goroutine 处理每个连接(支持并发)
+		//go HandleConnection(conn)
+		//clientAddr := conn.RemoteAddr().String()
+
+		buf := make([]byte, 1024)
+		for {
+			n, err := conn.Read(buf)
+			if n > 0 {
+				//安全截取实际读到的字节
+				//data := buf[:n]
+
+				//Set master = true
+				//if data[8] == 0x01 && data[5] == 0xf1 {
+				//	active.Master = true
+				//return // set to master , stop recv
+				//} // else if data[8] == 0x00 && data[5] == 0xf1 {
+				//active.Master = false
+				//}
+				//lfshook.NewLogger().Logger.Infof("Client  received %d bytes: hex=%x", n, data)
+
+				//Respond to remote
+				//if active.Master == true {
+				if _, werr := conn.Write([]byte("1")); werr != nil {
+					lfshook.NewLogger().Logger.Infof("Failed to write 'ok' to client: %+v", werr)
+				}
+				//} else {
+				//	if _, werr := conn.Write([]byte("0")); werr != nil {
+				//lfshook.NewLogger().Logger.Infof("Failed to write 'ok' to client: %+v", werr)
+				//	}
+				//}
+
+			}
+
+			if err == io.EOF {
+				//lfshook.NewLogger().Logger.Infof("Client %s 连接正常关闭", clientAddr)
+				break
+			}
+			if err != nil {
+				// 忽略临时错误(如 timeout),但记录非临时错误
+				if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+					continue // 可选:超时后继续读(需配合 SetReadDeadline)
+				}
+				//lfshook.NewLogger().Logger.Infof("addr:%s read error: %+v", clientAddr, err)
+				break
+			}
+		}
+		lfshook.NewLogger().Logger.Infof("Connection closed from cab1 set Master = true !")
+		active.Master = true
+		//return
+	}
+}
+*/
+/*
+func SendToRemoteMaster(ctx context.Context, conn net.Conn) {
+
+	protocol := msgdata.NewProtocol()
+	protocol.MessageID = 0xf1 //check master data type
+	protocol.DataLength = 0x02
+	protocol.Data = make([]byte, 2)
+
+	// 初始化协议...
+	ticker := time.NewTicker(2 * time.Second)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+
+		case <-ticker.C:
+
+			//set master data
+			//if active.Master {
+			//protocol.Data[0] = 0 //set remote master false
+			//} else {
+			protocol.Data[0] = 1 //set remote master true
+			//}
+
+			// 编码并发送数据...
+			encoded, err := protocol.Encode()
+			if err != nil {
+
+				return
+			}
+
+			conn.SetWriteDeadline(time.Now().Add(1000 * time.Millisecond)) //发送1秒超时
+			if conn != nil {
+				_, err = conn.Write(encoded)
+				if err != nil {
+					conn.Close()
+
+					active.Master = true
+					return // 触发重连
+				}
+			}
+
+			conn.SetReadDeadline(time.Now().Add(1000 * time.Millisecond)) //接收1秒超时
+			buf := make([]byte, 64)                                       // 足够接收短响应
+			n, err := conn.Read(buf)
+			if err != nil {
+				if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+					lfshook.NewLogger().Logger.Infof("Read timeout waiting for remote response — may be OK (no response expected?)")
+				} else if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "broken pipe") {
+					lfshook.NewLogger().Logger.Infof("Remote closed connection during read: %+v", err)
+					conn.Close()
+					return
+				} else {
+					lfshook.NewLogger().Logger.Infof("Read from remote failed unexpectedly: %+v", err)
+				}
+				active.Master = true
+				continue // 不中断循环,继续下一轮
+			}
+
+			//respond ok
+			if n > 0 {
+				continue
+				//response := strings.TrimSpace(string(buf[:n]))
+				//lfshook.NewLogger().Logger.Infof("Remote responded: [%s]", response)
+				//可选:校验响应,例如
+				//if response == "1" {
+				//	active.Master = false
+
+				//	if utils.CheckAsterisk() { //check asterisk available then stop asterisk
+				//		utils.ExecCmd("/etc/init.d/asterisk", "stop", "PBX")
+				//	}
+				//}
+			}
+		}
+	}
+}
+*/

+ 215 - 0
internal/app/stc/priority/index.go

@@ -0,0 +1,215 @@
+package priority
+
+import (
+	"os"
+	"pbx-api-gin/pkg/utils"
+	"strconv"
+	"sync"
+
+	"gopkg.in/ini.v1"
+)
+
+// PAD 是否接通
+var ICPAnswer = 0
+var OCCAnswer = 0
+
+var SpecialVoice = 0
+
+// PAD是否发起
+// var PADStart = 0
+// var PADTMSStart = 0
+var PADOccStart = 0
+
+// 是否有任务正在创建
+var TaskCreating = ""
+
+// 被打断的PAD类型
+var InterruptedPad = ""
+
+// 是否有CABCAB/PA打断其他(DTMF #/*)
+var CABInterrupt = 0
+var PAInterrupt = 0
+
+// 是否有CPA激活
+var CPAActived = 0
+
+var filePath = "/etc/asterisk/priority.conf"
+
+type TaskInfo struct {
+	RunChannel   string `json:"runChannel"`
+	LocalChan    string `json:"localChan"`
+	RunType      string `json:"runType"`
+	Priority     int    `json:"priority"`
+	ConfbridgeID string `json:"confbridgeID"`
+	Running      bool
+}
+
+type TaskPriority struct {
+	Priority int `json:"priority"`
+}
+
+type Tasks struct {
+	PA     TaskPriority `json:"pa"`
+	C2C    TaskPriority `json:"c2c"`
+	PADICP TaskPriority `json:"padicp"`
+	PADTMS TaskPriority `json:"padtms"`
+	PADOCC TaskPriority `json:"padocc"`
+	CPA    TaskPriority `json:"cpa"`
+	EMG    TaskPriority `json:"emg"`
+	SPC    TaskPriority `json:"spc"`
+	DCS    TaskPriority `json:"dcs"`
+	STN    TaskPriority `json:"stn"`
+	CHK    TaskPriority `json:"chk"`
+	VOL    TaskPriority `json:"vol"`
+}
+
+var AllTasks Tasks
+
+func GetPriority() {
+	utils.LoggerDebug.Printf("Init system priority !")
+
+	_, err := os.Stat(filePath)
+	if err != nil {
+		utils.LoggerDebug.Printf("GetPriority err :%+v", err)
+		return
+	}
+	iniFile, err := ini.Load(filePath)
+	if err != nil {
+		utils.LoggerDebug.Printf("GetPriority err :%+v", err)
+		return
+	}
+
+	AllTasks.CHK.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("CHK").Value())
+	AllTasks.STN.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("STN").Value())
+	AllTasks.DCS.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("DCS").Value())
+	AllTasks.SPC.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("SPC").Value())
+	AllTasks.EMG.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("EMG").Value())
+	AllTasks.CPA.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("CPA").Value())
+	AllTasks.PADOCC.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("PAD-OCC").Value())
+	AllTasks.PADTMS.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("PAD-TMS").Value())
+	AllTasks.PADICP.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("PAD-ICP").Value())
+	AllTasks.C2C.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("CabCab").Value())
+	AllTasks.PA.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("ManuPa").Value())
+	AllTasks.VOL.Priority, _ = strconv.Atoi(iniFile.Section("general").Key("VOL").Value())
+
+}
+
+func GetPriorityByKey(key string) int {
+
+	switch key {
+	case "VOL":
+		return AllTasks.VOL.Priority
+	case "CHK":
+		return AllTasks.CHK.Priority
+	case "STN":
+		return AllTasks.STN.Priority
+	case "DCS":
+		return AllTasks.DCS.Priority
+	case "SPC":
+		return AllTasks.SPC.Priority
+	case "EMG":
+		return AllTasks.EMG.Priority
+	case "CPA":
+		return AllTasks.CPA.Priority
+	case "PAD-OCC":
+		return AllTasks.PADOCC.Priority
+	case "PAD-TMS":
+		return AllTasks.PADTMS.Priority
+	case "PAD-ICP":
+		return AllTasks.PADICP.Priority
+	case "CabCab":
+		return AllTasks.C2C.Priority
+	case "ManuPa":
+		return AllTasks.PA.Priority
+	default:
+		return 0
+	}
+}
+
+var checkMutex sync.Mutex
+
+// check priority , if the running priority is lowwer than the to run priority
+func CheckPriority(runType string) bool {
+	utils.LoggerDebug.Printf("CheckPriority  TorunType:%s 	SpecialVoice:%d", runType, SpecialVoice)
+	checkMutex.Lock()
+	defer checkMutex.Unlock()
+
+	var taskRuning TaskInfo
+	var runingtaskName string
+	var ok bool
+
+	//Check special voice can not interrupt
+	if SpecialVoice == 1 {
+		return false
+	}
+
+	//Get the to run priority number in the config file
+	toRunpriority := GetPriorityByKey(runType)
+
+	//获取当前优先级最高的任务信息
+	if runType != "PA" && runType != "PAD-ICP" && runType != "PAD-TMS" { // ignore C2C
+		runingtaskName, taskRuning, ok = RegistryTask.HighestPriorityRunningTask1()
+		if !ok {
+			return true
+		}
+	} else {
+		runingtaskName, taskRuning, ok = RegistryTask.HighestPriorityRunningTask()
+		if !ok {
+			return true
+		}
+	}
+
+	if ok {
+		utils.LoggerDebug.Printf("CheckPriority  TorunType:%s 	SpecialVoice:%d 	toRunpriority:%d 	runingpriority:%d ", runType, SpecialVoice, toRunpriority, taskRuning.Priority)
+		//if the running task priority is lowwer
+		if toRunpriority < taskRuning.Priority && toRunpriority != 0 {
+			return true
+
+		} else if toRunpriority == taskRuning.Priority {
+
+			switch runType {
+			case "PAD-ICP":
+				return true
+			case "PAD-TMS":
+				return true
+			case "EMG":
+				return true
+			case "SPC":
+				return true
+			case "DCS":
+				if taskRuning.RunType == "STN" {
+					return false
+				} else {
+					return true
+				}
+
+			case "STN":
+				if taskRuning.RunType == "DCS" {
+					return false
+				} else {
+					return true
+				}
+			case "CHK":
+				return true
+			case "VOL":
+				return true
+			}
+		}
+
+		if runingtaskName == "C2C" {
+			if runType != "PA" && runType != "PAD-ICP" && runType != "PAD-TMS" && runType != "C2C" {
+				return true
+			}
+		} else if runType == "C2C" {
+			if runingtaskName != "PA" && runingtaskName != "PAD-ICP" && runingtaskName != "PAD-TMS" && runingtaskName != "C2C" {
+				return true
+			}
+		}
+
+	} else {
+		//No any runing task now
+		return true
+	}
+
+	return false
+}

+ 137 - 0
internal/app/stc/priority/task.go

@@ -0,0 +1,137 @@
+package priority
+
+import (
+	"pbx-api-gin/pkg/utils"
+	"sync"
+)
+
+// 全局导出变量:所有导入 pro 包的代码均可直接使用 pro.Registry
+var RegistryTask = NewTaskRegistry()
+
+// TaskRegistry:线程安全的任务注册表,仅保存 Running==true 的任务
+type TaskRegistry struct {
+	mu sync.RWMutex
+	m  map[string]TaskInfo // key: task name (e.g., "PA", "C2C", "DTMF")
+}
+
+// NewTaskRegistry 创建新注册表
+func NewTaskRegistry() *TaskRegistry {
+	return &TaskRegistry{
+		m: make(map[string]TaskInfo),
+	}
+}
+
+// Register 启动并注册一个任务(仅当 Running == true 时才存入)
+func (r *TaskRegistry) Register(name string, t TaskInfo) {
+	if !t.Running {
+		return // 不注册未运行的任务
+	}
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	r.m[name] = t
+}
+
+// StopAndUnregister 标记任务为停止,并立即从注册表中删除(推荐在任务结束时调用)
+func (r *TaskRegistry) StopAndUnregister(name string) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	delete(r.m, name)
+}
+
+// UpdateStatus 更新任务状态;若设为 false,则自动删除(满足你“任务结束即删除”的需求)
+func (r *TaskRegistry) UpdateStatus(name string, newStatus bool) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	if !newStatus {
+		//delete(r.m, name) //:Running=false
+		if t, ok := r.m[name]; ok {
+			t.Running = false
+			r.m[name] = t
+		} else {
+			// 如果没有旧记录,但你要“重新启用”,需外部提供完整 TaskInfo → 建议用 Register()
+		}
+		return
+	}
+
+	// 若 newStatus == true,尝试更新或插入(需原 task info 完整)
+	if t, ok := r.m[name]; ok {
+		t.Running = true
+		r.m[name] = t
+	} else {
+		// 如果没有旧记录,但你要“重新启用”,需外部提供完整 TaskInfo → 建议用 Register()
+	}
+}
+
+// Get 获取指定任务(nil if not found or not running)
+func (r *TaskRegistry) Get(name string) (TaskInfo, bool) {
+	r.mu.RLock()
+	defer r.mu.RUnlock()
+	t, ok := r.m[name]
+	return t, ok && t.Running
+}
+
+// ListAll 返回所有当前运行中的任务副本(安全遍历)
+func (r *TaskRegistry) ListAll() []TaskInfo {
+	r.mu.RLock()
+	defer r.mu.RUnlock()
+	list := make([]TaskInfo, 0, len(r.m))
+	for _, t := range r.m {
+		if t.Running { // 冗余检查,确保一致性
+			list = append(list, t)
+		}
+	}
+	return list
+}
+
+// HighestPriorityRunningTask returns the task with smallest Priority value,
+// which means highest scheduling priority (e.g., Priority=0 > 1 > 10).
+// Returns (key, task, found). If no task exists, found is false.
+func (r *TaskRegistry) HighestPriorityRunningTask() (string, TaskInfo, bool) {
+
+	r.mu.RLock()
+	defer r.mu.RUnlock()
+
+	var bestKey string
+	var best TaskInfo
+	found := false
+
+	for key, task := range r.m {
+		if !found || task.Priority < best.Priority {
+			bestKey = key
+			best = task
+			found = true
+		}
+	}
+	utils.LoggerDebug.Printf("HighestPriorityRunningTask Get task:%+v", best)
+	return bestKey, best, found
+}
+
+// HighestPriorityRunningTask1 returns the task with smallest Priority value except C2C,
+// which means highest scheduling priority (e.g., Priority=0 > 1 > 10).
+// Returns (key, task, found). If no task exists, found is false.
+func (r *TaskRegistry) HighestPriorityRunningTask1() (string, TaskInfo, bool) {
+
+	r.mu.RLock()
+	defer r.mu.RUnlock()
+
+	var bestKey string
+	var best TaskInfo
+	found := false
+
+	for key, task := range r.m {
+		if task.RunType == "C2C" {
+			continue
+		}
+
+		if !found || task.Priority < best.Priority {
+			bestKey = key
+			best = task
+			found = true
+		}
+	}
+
+	//lfshook.NewLogger().Infof("====HighestPriorityRunningTask1 ret=====%s", best.RunType)
+	utils.LoggerDebug.Printf("HighestPriorityRunningTask1 Get task:%+v", best)
+	return bestKey, best, found
+}

+ 321 - 0
internal/app/stc/sendstatus/status.go

@@ -0,0 +1,321 @@
+package alstatus
+
+import (
+	"fmt"
+	"net"
+	"pbx-api-gin/internal/app/stc/active"
+	msgdata "pbx-api-gin/internal/app/stc/data"
+	"pbx-api-gin/internal/app/stc/priority"
+	"pbx-api-gin/internal/app/stc/socket"
+	"pbx-api-gin/pkg/utils"
+	"time"
+)
+
+type PADQueue struct {
+	Exten      string    `json:"exten"`
+	ActiveTime time.Time `json:"activeTime"`
+}
+
+var PadQueues []PADQueue
+
+// 删除某个报警器
+func removeAllByExten(queues []PADQueue, target string) []PADQueue {
+	result := make([]PADQueue, 0, len(queues))
+	for _, q := range queues {
+		if q.Exten != target {
+			result = append(result, q)
+		}
+	}
+	return result
+}
+
+// 添加报警器
+func addExtenToQueue(queues []PADQueue, target string) []PADQueue {
+
+	var padInfo = PADQueue{Exten: target, ActiveTime: time.Now()}
+
+	for _, q := range queues {
+		if q.Exten == target {
+			return queues
+		}
+	}
+	queues = append(queues, padInfo)
+	return queues
+}
+
+// 初始化pad队列
+func init() {
+	PadQueues = make([]PADQueue, 0)
+}
+
+func SendToStc(conn net.Conn, data []byte) {
+
+	_, err := conn.Write(data)
+	if err != nil {
+		utils.LoggerDebug.Printf("Send To STC msg err:%+v", err)
+		conn.Close()
+	}
+	//lfshook.NewLogger().Logger.Infof("SendToStc data:%x", data)
+}
+
+// report alarm status to STC
+func AlarmStatus(exten string, status string) {
+
+	//Not Master role , ignore
+	if !active.Master {
+		return
+	}
+
+	//check exten if it is a alarm exten
+	if !utils.IsPAIU(exten) { // if not alarm device , return
+		return
+	}
+
+	protocol := msgdata.NewProtocol()
+	protocol.MessageID = 0x26
+	protocol.DataLength = 0x04
+	protocol.Data = make([]byte, 4)
+
+	protocol.Data[0] = exten[2] - '0' //车厢号
+	protocol.Data[1] = exten[3] - '0' //位置号
+
+	//报警器工作状态
+	switch status {
+	case "unavailable", "Unavailable": //offline
+		protocol.Data[2] = 0x00
+
+	case "idle", "Idle": //idle
+		protocol.Data[2] = 0x01
+
+	case "dial": //dial
+		protocol.Data[2] = 0x02
+		return
+	case "queue": //PAD alarm
+		protocol.Data[2] = 0x03
+
+	case "connect": //connect
+		protocol.Data[2] = 0x04
+
+	case "allreset": //allreset
+
+		protocol.MessageID = 0x29
+		protocol.Data[0] = 0x02 //all reset
+		protocol.Data[1] = 0
+		protocol.Data[2] = 0
+	case "allhold": //allhold
+
+		protocol.MessageID = 0x29
+		protocol.Data[0] = 0x01 //all hold
+		protocol.Data[1] = 0
+		protocol.Data[2] = 0
+	}
+
+	//将pad存储到队列中
+	if protocol.Data[2] == 0x03 { //排队状态
+		PadQueues = addExtenToQueue(PadQueues, exten)
+		utils.LoggerDebug.Printf("PAD:%s add to queue", exten)
+	} else if protocol.Data[2] == 0x01 { //空闲状态
+		utils.LoggerDebug.Printf("PAD:%s del from queue", exten)
+		PadQueues = removeAllByExten(PadQueues, exten)
+	}
+
+	encoded, errEn := protocol.Encode()
+	if errEn != nil {
+		fmt.Println("Encode error:", errEn)
+		return
+	}
+	//check if actived
+	utils.LoggerDebug.Printf("PAD number:%s		CarNum:%x 	 Pos:%x 	Status:%x(0=Offline,1=Idle,2=calling,3=Hold,4=Connected)", exten, protocol.Data[0], protocol.Data[1], protocol.Data[2])
+
+	if socket.Conn != nil {
+		SendToStc(socket.Conn, encoded)
+	}
+
+	if socket.Conn8 != nil {
+		SendToStc(socket.Conn8, encoded)
+	}
+
+}
+
+// report broadcast status to STC
+func PaStatus(src string, patype string, operation string) {
+
+	//Not Master role , ignore
+	if !active.Master {
+		return
+	}
+
+	//过滤掉非EMG运行模式下的continue状态发送
+	if operation == "continue" {
+		taskName, _, _ := priority.RegistryTask.HighestPriorityRunningTask()
+		if taskName != "EMG" {
+			return
+		}
+	}
+
+	utils.LoggerDebug.Printf("PA Status Src:%s 		Type:%s		Status:%s", src, patype, operation)
+	protocol := msgdata.NewProtocol()
+	protocol.MessageID = 0x22
+	protocol.DataLength = 0x04
+	protocol.Data = make([]byte, 4)
+
+	//广播发起方
+	switch src {
+	case "2311": //mc1
+		protocol.Data[0] = 0x01
+	case "2381": //mc8
+		protocol.Data[0] = 0x08
+	/*case "1411": //mc8
+		protocol.Data[0] = 0x08
+	case "1481": //mc8
+		protocol.Data[0] = 0x08*/
+	default: //
+		protocol.Data[0] = 0x00
+	}
+
+	//广播类型
+	switch patype {
+	case "C2C": //司机对讲---ami
+		protocol.Data[1] = 0x01
+	case "PA": //人工广播---ami
+		protocol.Data[1] = 0x02
+	case "DCS": //开关门提示音
+		protocol.Data[1] = 0x03
+	case "EMG": //紧急广播
+		protocol.Data[1] = 0x04
+	case "PAD": //报警
+		protocol.Data[1] = 0x05
+	case "CPA": //地面广播---ami
+		protocol.Data[1] = 0x06
+	case "SPC": //特殊
+		protocol.Data[1] = 0x07
+	case "STN": //报站
+		protocol.Data[1] = 0x08
+	case "CHK": //自检
+		protocol.Data[1] = 0x09
+	case "VOL": //音调条件
+		protocol.Data[1] = 0x0a
+	}
+
+	//操作类型
+	switch operation {
+	case "start": //
+		protocol.Data[2] = 0x01
+	case "end": //
+		protocol.Data[2] = 0x02
+	case "refuse": //
+		protocol.Data[2] = 0x03
+	case "fail": //
+		protocol.Data[2] = 0x04
+	case "continue": //
+		protocol.Data[2] = 0x05
+	}
+
+	encoded, errEn := protocol.Encode()
+	if errEn != nil {
+		utils.LoggerDebug.Printf("Encode error:%+v", errEn)
+		return
+	}
+
+	if socket.Conn != nil {
+		SendToStc(socket.Conn, encoded)
+	}
+
+	if socket.Conn8 != nil {
+		SendToStc(socket.Conn8, encoded)
+	}
+
+}
+
+// report broadcast status to STC
+func OccPad(operation string) {
+
+	//Not Master role , ignore
+	if !active.Master {
+		return
+	}
+
+	utils.LoggerDebug.Printf("OCC-PAD status:%s", operation)
+	protocol := msgdata.NewProtocol()
+	protocol.MessageID = 0x2A
+	protocol.DataLength = 0x04
+	protocol.Data = make([]byte, 4)
+
+	//广播发起方
+	switch operation {
+	case "start": //mc1
+		protocol.Data[0] = 0x01
+	case "end": //mc8
+		protocol.Data[0] = 0x02
+	case "": //
+		protocol.Data[0] = 0x00
+	}
+
+	encoded, errEn := protocol.Encode()
+	if errEn != nil {
+		utils.LoggerDebug.Printf("Encode error:%+v", errEn)
+		return
+	}
+
+	if socket.Conn != nil {
+		SendToStc(socket.Conn, encoded)
+	}
+
+	if socket.Conn8 != nil {
+		SendToStc(socket.Conn8, encoded)
+	}
+
+}
+
+// report broadcast status to STC
+func SendRecordFile(filename, rcdtype string) {
+
+	//time.Sleep(5 * time.Second)
+	/*
+		if !utils.FileExists(filename) {
+			lfshook.NewLogger().Logger.Infof("===Recording filename not exist:%+v=", filename)
+			return
+		}
+	*/
+	//Not Master role , ignore
+	if !active.Master {
+		return
+	}
+
+	protocol := msgdata.NewProtocol()
+	protocol.MessageID = 0x31
+
+	filenameHex := []byte(filename)
+	dataLen := len(filenameHex) + 1
+
+	protocol.DataLength = uint16(dataLen)
+	protocol.Data = make([]byte, dataLen)
+	copy(protocol.Data[1:], filenameHex)
+
+	switch rcdtype {
+	case "C2C": //
+		protocol.Data[0] = 0x01
+	case "PA": //
+		protocol.Data[0] = 0x02
+	case "PAD": //
+		protocol.Data[0] = 0x05
+	case "CPA": //
+		protocol.Data[0] = 0x06
+	case "OTR": //
+		protocol.Data[0] = 0x03
+		//lfshook.NewLogger().Logger.Infof("===Recording filename:%+v=", protocol.Data)
+	}
+	encoded, errEn := protocol.Encode()
+	if errEn != nil {
+		utils.LoggerDebug.Printf("Encode error:%+v", errEn)
+		return
+	}
+
+	if socket.Conn != nil {
+		SendToStc(socket.Conn, encoded)
+	}
+
+	if socket.Conn8 != nil {
+		SendToStc(socket.Conn8, encoded)
+	}
+}

+ 99 - 0
internal/app/stc/socket/index.go

@@ -0,0 +1,99 @@
+package socket
+
+import (
+	"fmt"
+	"net"
+	"pbx-api-gin/internal/app/stc/active"
+	"pbx-api-gin/pkg/utils"
+	"time"
+)
+
+var Conn net.Conn
+var Conn8 net.Conn
+
+var ConnToSlave net.Conn
+
+const RemotePort = 10100
+const LocalPort = 10201
+const LocalPort8 = 10202
+
+const LocalServerPort = 10000
+
+const RemoteAddr = "10.0.11.11"
+
+const RemoteAddr8 = "10.0.11.81"
+
+func IsIPExists(targetIP string) (bool, error) {
+	// 解析目标 IP
+	utils.LoggerDebug.Printf("Checking device IP , Set CabNumber ......")
+	ip := net.ParseIP(targetIP)
+	if ip == nil {
+		return false, fmt.Errorf("invalid IP address: %s", targetIP)
+	}
+
+	interfaces, err := net.Interfaces()
+	if err != nil {
+		return false, err
+	}
+
+	for _, iface := range interfaces {
+		addrs, err := iface.Addrs()
+		if err != nil {
+			continue // 忽略无法读取地址的接口
+		}
+		for _, addr := range addrs {
+			if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP != nil {
+				// 精确匹配 IP(忽略掩码,只看地址本身是否一致)
+				if ipnet.IP.Equal(ip) ||
+					(ip.To4() != nil && ipnet.IP.To4() != nil && ipnet.IP.To4().Equal(ip.To4())) {
+					return true, nil
+				}
+			}
+		}
+	}
+	return false, nil
+}
+
+// Get eth0 IP
+func SetMasterCabNum() {
+
+	count := 0
+
+getIP:
+
+	utils.LoggerDebug.Printf("Checking system IP  ....")
+	ext1, err := IsIPExists("10.0.11.11")
+	if err != nil {
+		utils.LoggerDebug.Printf("Check IP 10.0.11.11 err :%+v", err)
+	}
+	// Init cab1 number and master role
+	if ext1 {
+		active.CabNum = "1"
+		active.Master = true
+		return
+	}
+
+	ext8, err := IsIPExists("10.0.11.81")
+	if err != nil {
+		utils.LoggerDebug.Printf("Check IP 10.0.11.81 err:%+v", err)
+	}
+	// Init cab8 number and master role
+	if ext8 {
+		active.CabNum = "8"
+		active.Master = false
+		return
+	}
+
+	if !(ext1 || ext8) {
+		if count < 5 {
+			count++
+			time.Sleep(time.Second * 2)
+			goto getIP
+		}
+		utils.LoggerDebug.Printf("Can not get device IP ! Set Cabnum=1  Master=true by default .")
+
+		active.CabNum = "1"
+		active.Master = true
+	}
+
+}

+ 67 - 0
internal/pkg/configs/decode.go

@@ -0,0 +1,67 @@
+package configs
+
+import (
+	"io/ioutil"
+	"os"
+	"pbx-api-gin/pkg/lfshook"
+
+	log "github.com/sirupsen/logrus"
+
+	"gopkg.in/yaml.v2"
+)
+
+// Config https://godoc.org/gopkg.in/yaml.v2
+// Config 存储配置
+type Config struct {
+	//IdentityKey string `yaml:"identityKey"`
+
+	AsteriskAMIHost   string `yaml:"asteriskAMIHost"`   // Host
+	AsteriskAMIPort   string `yaml:"asteriskAMIPort"`   // Port
+	AsteriskAMIUser   string `yaml:"asteriskAMIUser"`   // User
+	AsteriskAMISecret string `yaml:"asteriskAMISecret"` // Secret
+
+	LogInfoPath    string `yaml:"logInfoPath"`    //logInfoPath
+	LogErrorPath   string `yaml:"logErrorPath"`   //logErrorPath
+	RecordEventLog string `yaml:"recordingEvent"` //recordingEventfile
+	DebugLogPath   string `yaml:"debugLog"`       //debugLog
+	ProcessRecord  string `yaml:"processRecord"`  //processRecord
+
+	LogLevel log.Level //logLevel
+}
+
+// ConfigPath 配置文件路径
+var ConfigPath = "/data/PA/configs/config.yaml"
+
+// ConfigGlobal 全局配置变量
+var ConfigGlobal *Config
+
+// DecodeConfig 解析配置
+func DecodeConfig() {
+
+	_, err := os.Stat(ConfigPath)
+	if err != nil {
+		lfshook.NewLogger().Errorf("config file not exist %+v", err)
+		return
+	}
+
+	fileByte, err := ioutil.ReadFile(ConfigPath)
+	if err != nil {
+		lfshook.NewLogger().Errorf("read config file %+v", err)
+	}
+
+	ConfigGlobal = &Config{}
+	err = yaml.Unmarshal(fileByte, ConfigGlobal)
+	if err != nil {
+		lfshook.NewLogger().Errorf("Unmarshal config file %+v", err)
+	}
+}
+
+// EncodeConfig 保存配置
+func EncodeConfig() error {
+	out, _ := yaml.Marshal(ConfigGlobal)
+	err := ioutil.WriteFile(ConfigPath, out, 0777)
+	if err != nil {
+		lfshook.NewLogger().Errorf("save config file %+v", err)
+	}
+	return err
+}

+ 218 - 0
pkg/lfshook/log.go

@@ -0,0 +1,218 @@
+// Package lfshook is hook for sirupsen/logrus that used for writing the logs to local files.
+package lfshook
+
+import (
+	"fmt"
+	"io"
+	"reflect"
+	"sync"
+
+	"github.com/sirupsen/logrus"
+	"gopkg.in/natefinch/lumberjack.v2"
+)
+
+// We are logging to file, strip colors to make the output more readable.
+var defaultFormatter = &logrus.TextFormatter{DisableColors: true}
+
+// PathMap is map for mapping a log level to a file's path.
+// Multiple levels may share a file, but multiple files may not be used for one level.
+// type PathMap map[logrus.Level]string
+
+// 一个 level 对应 一个 logger
+type LoggerMap map[logrus.Level]*lumberjack.Logger
+
+// WriterMap is map for mapping a log level to an io.Writer.
+// Multiple levels may share a writer, but multiple writers may not be used for one level.
+type WriterMap map[logrus.Level]io.Writer
+
+type RotateFileConfig struct {
+	Filename   string
+	MaxSize    int
+	MaxBackups int
+	MaxAge     int
+	Level      logrus.Level
+	Formatter  logrus.Formatter
+}
+
+// LfsHook is a hook to handle writing to local log files.
+type LfsHook struct {
+	loggers   LoggerMap
+	writers   WriterMap
+	levels    []logrus.Level
+	lock      *sync.Mutex
+	formatter logrus.Formatter
+
+	Config RotateFileConfig
+
+	defaultPath      string
+	defaultWriter    io.Writer
+	hasDefaultPath   bool
+	hasDefaultWriter bool
+}
+
+// NewHook returns new LFS hook.
+// Output can be a string, io.Writer, WriterMap or PathMap.
+// If using io.Writer or WriterMap, user is responsible for closing the used io.Writer.
+func NewHook(output interface{}, formatter logrus.Formatter) *LfsHook {
+	hook := &LfsHook{
+		lock: new(sync.Mutex),
+	}
+
+	hook.SetFormatter(formatter)
+
+	switch output.(type) {
+	case string:
+		hook.SetDefaultPath(output.(string))
+		break
+	case io.Writer:
+		hook.SetDefaultWriter(output.(io.Writer))
+		break
+	case LoggerMap:
+		hook.loggers = output.(LoggerMap)
+		for level := range output.(LoggerMap) {
+			hook.levels = append(hook.levels, level)
+		}
+		break
+	case WriterMap:
+		hook.writers = output.(WriterMap)
+		for level := range output.(WriterMap) {
+			hook.levels = append(hook.levels, level)
+		}
+		break
+	default:
+		panic(fmt.Sprintf("unsupported level map type: %v", reflect.TypeOf(output)))
+	}
+
+	return hook
+}
+
+// SetFormatter sets the format that will be used by hook.
+// If using text formatter, this method will disable color output to make the log file more readable.
+func (hook *LfsHook) SetFormatter(formatter logrus.Formatter) {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	if formatter == nil {
+		formatter = defaultFormatter
+	} else {
+		switch formatter.(type) {
+		case *logrus.TextFormatter:
+			textFormatter := formatter.(*logrus.TextFormatter)
+			textFormatter.DisableColors = true
+		}
+	}
+
+	hook.formatter = formatter
+}
+
+// SetDefaultPath sets default path for levels that don't have any defined output path.
+func (hook *LfsHook) SetDefaultPath(defaultPath string) {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	hook.defaultPath = defaultPath
+	hook.hasDefaultPath = true
+}
+
+// SetDefaultWriter sets default writer for levels that don't have any defined writer.
+func (hook *LfsHook) SetDefaultWriter(defaultWriter io.Writer) {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	hook.defaultWriter = defaultWriter
+	hook.hasDefaultWriter = true
+}
+
+// Fire writes the log file to defined path or using the defined writer.
+// User who run this function needs write permissions to the file or directory if the file does not yet exist.
+func (hook *LfsHook) Fire(entry *logrus.Entry) error {
+	hook.lock.Lock()
+	defer hook.lock.Unlock()
+	if hook.writers != nil || hook.hasDefaultWriter {
+		return hook.ioWrite(entry)
+	} else if hook.loggers != nil || hook.hasDefaultPath {
+		return hook.fileWrite(entry)
+	}
+
+	return nil
+}
+
+// Write a log line to an io.Writer.
+func (hook *LfsHook) ioWrite(entry *logrus.Entry) error {
+	var (
+		writer io.Writer
+		msg    []byte
+		err    error
+		ok     bool
+	)
+
+	if writer, ok = hook.writers[entry.Level]; !ok {
+		if hook.hasDefaultWriter {
+			writer = hook.defaultWriter
+		} else {
+			return nil
+		}
+	}
+
+	// use our formatter instead of entry.String()
+	msg, err = hook.formatter.Format(entry)
+
+	if err != nil {
+		log.Println("failed to generate string for entry:", err)
+		return err
+	}
+	_, err = writer.Write(msg)
+	return err
+}
+
+// Write a log line directly to a file.
+func (hook *LfsHook) fileWrite(entry *logrus.Entry) error {
+	var (
+		logger *lumberjack.Logger
+		msg    []byte
+		err    error
+		ok     bool
+	)
+
+	if logger, ok = hook.loggers[entry.Level]; !ok {
+		if hook.hasDefaultPath {
+			logger = &lumberjack.Logger{
+				Filename:   hook.defaultPath,
+				MaxSize:    10, // maxSize M
+				MaxBackups: 5,  // keep 5 file
+				MaxAge:     7,  //  7 day
+			}
+		} else {
+			return nil
+		}
+	}
+
+	// use our formatter instead of entry.String()
+	msg, err = hook.formatter.Format(entry)
+
+	if err != nil {
+		log.Println("failed to generate string for entry:", err)
+		return err
+	}
+
+	defer logger.Close()
+	logger.Write(msg)
+	return nil
+}
+
+// Levels returns configured log levels.
+func (hook *LfsHook) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+
+var log *logrus.Entry
+var once sync.Once
+
+//NewLogger 初始化 logger
+func NewLogger() *logrus.Entry {
+	once.Do(func() {
+		log = logrus.New().WithFields(
+			logrus.Fields{
+				"appname": "pbx-panel",
+			},
+		)
+	})
+	return log
+}

+ 96 - 0
pkg/systeminfo/time.go

@@ -0,0 +1,96 @@
+package systeminfo
+
+import (
+	"fmt"
+	"strings"
+	"time"
+)
+
+// Seconds-based time units
+const (
+	Minute = 60
+	Hour   = 60 * Minute
+	Day    = 24 * Hour
+	Week   = 7 * Day
+	Month  = 30 * Day
+	Year   = 12 * Month
+)
+
+func computeTimeDiff(diff int64) (int64, string) {
+	diffStr := ""
+	switch {
+	case diff <= 0:
+		diff = 0
+		diffStr = "now"
+	case diff < 2:
+		diff = 0
+		diffStr = "1 second"
+	case diff < 1*Minute:
+		diffStr = fmt.Sprintf("%d seconds", diff)
+		diff = 0
+
+	case diff < 2*Minute:
+		diff -= 1 * Minute
+		diffStr = "1 minute"
+	case diff < 1*Hour:
+		diffStr = fmt.Sprintf("%d minutes", diff/Minute)
+		diff -= diff / Minute * Minute
+
+	case diff < 2*Hour:
+		diff -= 1 * Hour
+		diffStr = "1 hour"
+	case diff < 1*Day:
+		diffStr = fmt.Sprintf("%d hours", diff/Hour)
+		diff -= diff / Hour * Hour
+
+	case diff < 2*Day:
+		diff -= 1 * Day
+		diffStr = "1 day"
+	case diff < 1*Week:
+		diffStr = fmt.Sprintf("%d days", diff/Day)
+		diff -= diff / Day * Day
+
+	case diff < 2*Week:
+		diff -= 1 * Week
+		diffStr = "1 week"
+	case diff < 1*Month:
+		diffStr = fmt.Sprintf("%d weeks", diff/Week)
+		diff -= diff / Week * Week
+
+	case diff < 2*Month:
+		diff -= 1 * Month
+		diffStr = "1 month"
+	case diff < 1*Year:
+		diffStr = fmt.Sprintf("%d months", diff/Month)
+		diff -= diff / Month * Month
+
+	case diff < 2*Year:
+		diff -= 1 * Year
+		diffStr = "1 year"
+	default:
+		diffStr = fmt.Sprintf("%d years", diff/Year)
+		diff = 0
+	}
+	return diff, diffStr
+}
+
+// TimeSincePro calculates the time interval and generate full user-friendly string.
+func TimeSincePro(then time.Time) string {
+	now := time.Now()
+	diff := now.Unix() - then.Unix()
+
+	if then.After(now) {
+		return "future"
+	}
+
+	var timeStr, diffStr string
+	for {
+		if diff == 0 {
+			break
+		}
+
+		diff, diffStr = computeTimeDiff(diff)
+		timeStr += ", " + diffStr
+	}
+	return strings.TrimPrefix(timeStr, ", ")
+}

+ 69 - 0
pkg/utils/cmd.go

@@ -0,0 +1,69 @@
+package utils
+
+import (
+	"bytes"
+	"context"
+	"os/exec"
+	"pbx-api-gin/pkg/lfshook"
+	"time"
+)
+
+// ExecCmdAsync 执行指定命令
+func ExecCmdAsync(cmdName string, arg ...string) (stdOut, errOut string, err error) {
+	cmd := exec.Command(cmdName, arg...)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err = cmd.Run()
+	if err != nil {
+		lfshook.NewLogger().Errorf("cmd.Run(%s) failed with %s\n", cmdName, err)
+	}
+	outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
+	if len(outStr) > 0 {
+		lfshook.NewLogger().Debugf("cmd.Run(%s) %s", cmdName, outStr)
+	}
+	if len(errStr) > 0 {
+		lfshook.NewLogger().Errorf("cmd.Run(%s)%s", cmdName, errStr)
+	}
+	return outStr, errStr, err
+}
+
+// ExecCmd 执行指定命令
+func ExecCmd(cmdName string, arg ...string) (stdOut, errOut string, err error) {
+	cmd := exec.Command(cmdName, arg...)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	err = cmd.Start()
+	if err != nil {
+		lfshook.NewLogger().Error(err)
+		return "", "", err
+	}
+
+	go func() {
+		err = cmd.Wait()
+		if err != nil {
+			lfshook.NewLogger().Errorf("cmd.Wait(%s) %s\n", cmdName, err)
+		} else {
+			lfshook.NewLogger().Infof("cmd.Wait(%s)\n", cmdName)
+		}
+		cancel()
+	}()
+
+	select {
+	case <-ctx.Done():
+	}
+
+	outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
+	if len(outStr) > 0 {
+		lfshook.NewLogger().Debugf("cmd.Start(%s) %s", cmdName, outStr)
+	}
+	if len(errStr) > 0 {
+		lfshook.NewLogger().Errorf("cmd.Start(%s)%s", cmdName, errStr)
+	}
+	return outStr, errStr, err
+}

+ 17 - 0
pkg/utils/common.go

@@ -0,0 +1,17 @@
+package utils
+
+import "fmt"
+
+var DialPrefix = "PJSIP"
+
+type Version struct {
+	GitCommitCode string `json:"gitCommitcode"`
+	GoVersion     string `json:"goVersion"`
+	BuildDate     string `json:"buildDate"`
+}
+
+func (version *Version) String() string {
+	return fmt.Sprintf("git commit code: %s\nbuild date: %s\ngo version: %s", version.GitCommitCode, version.BuildDate, version.GoVersion)
+}
+
+var VersionInstance = &Version{}

+ 22 - 0
pkg/utils/exit.go

@@ -0,0 +1,22 @@
+package utils
+
+import (
+	"os"
+	"os/signal"
+	"pbx-api-gin/pkg/lfshook"
+	"syscall"
+)
+
+func Exit() {
+	sigs := make(chan os.Signal, 1)
+	done := make(chan bool, 1)
+	// syscall.SIGSTOP windows not support
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+	go func() {
+		<-sigs
+		done <- true
+	}()
+	lfshook.NewLogger().Info("Server Start Awaiting Signal")
+	<-done
+	lfshook.NewLogger().Info("Exiting")
+}

+ 161 - 0
pkg/utils/file.go

@@ -0,0 +1,161 @@
+package utils
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"pbx-api-gin/internal/pkg/configs"
+	"pbx-api-gin/pkg/lfshook"
+	"strconv"
+	"strings"
+)
+
+// FileExists checks if a file exists and is not a directory before we
+// try using it to prevent further errors.
+func FileExists(filename string) bool {
+	info, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return !info.IsDir()
+}
+
+func GetDuration(filePath string) (int, error) {
+	cmd := exec.Command("ffprobe",
+		"-v", "quiet",
+		"-show_entries", "format=duration",
+		"-of", "default=nw=1:nokey=1",
+		filePath,
+	)
+
+	output, err := cmd.Output()
+	if err != nil {
+		lfshook.NewLogger().Logger.Infof("=======GetDuration err:%+v ! ============", output)
+		return 0, err
+	}
+
+	// 解析输出(纯数字,单位秒)
+	durationStr := strings.TrimSpace(string(output))
+	duration, err := strconv.ParseFloat(durationStr, 64)
+	if err != nil {
+		return 0, err
+	}
+
+	return int(duration), nil
+}
+
+func AudioFileEncode(dstFile, srcFile string) error {
+	// Step 1:获取 Base64 编码结果
+	data := "sripis123"
+	encoded := base64.StdEncoding.EncodeToString([]byte(data))
+
+	// Step 2: 创建一个新文件,写入 Base64 编码结果
+	outputFile, err := os.Create(dstFile)
+	if err != nil {
+		return err
+	}
+	defer outputFile.Close()
+
+	_, err = outputFile.WriteString(encoded)
+	if err != nil {
+		return err
+	}
+
+	// Step 3: 打开 audio.wav 文件
+	audioFile, err := os.Open(srcFile)
+	if err != nil {
+		return err
+	}
+	defer audioFile.Close()
+
+	// Step 4: 读取源文件的全部内容
+	data1, err := io.ReadAll(audioFile)
+	if err != nil {
+		return err
+	}
+
+	// Step 5: 对读取的内容进行 Base64 编码
+	encoded1 := base64.StdEncoding.EncodeToString(data1)
+
+	// Step 6: 写入 Base64 编码后的内容
+	_, err = outputFile.WriteString(encoded1)
+	if err != nil {
+		return err
+	}
+	return err
+}
+
+// ConvertAndSegmentWAV 将 WAV 文件转为 22kHz 并每 180 秒切分,返回生成的文件名列表
+func ConvertAndSegmentWAV(inputFile string, outputPrefix string) ([]string, error) {
+	// 输出文件命名模式,如:output_%03d.wav
+	outputPattern := outputPrefix + "_%03d.wav"
+
+	// 构建 ffmpeg 命令
+	cmd := exec.Command("ffmpeg",
+		"-i", inputFile, // 输入文件
+		"-ar", "22050", // 设置采样率为 22050 Hz (22kHz)
+		"-f", "segment", // 启用分段模式
+		"-segment_time", "180", // 每段 180 秒
+		"-c:a", "pcm_s16le", // 音频编码格式(标准 WAV)
+		"-reset_timestamps", "1", // 重置时间戳,每个片段从 0 开始
+		outputPattern, // 输出文件命名规则
+	)
+
+	// 执行命令并捕获输出
+	output, err := cmd.CombinedOutput()
+	if err != nil && !strings.Contains(string(output), "Conversion failed!") {
+		// 即使有警告(如非关键错误),只要文件生成了就继续解析
+		//log.Printf("ffmpeg 执行时有警告: %v", err)
+		lfshook.NewLogger().Logger.Infof("=======ConvertAndSegmentWAV err:%+v ! ============", output)
+	}
+	// 解析输出,提取所有生成的文件名
+	files := extractFilenamesFromFFmpegOutput(outputPrefix)
+
+	//lfshook.NewLogger().Infof("found after convert outfiles %+v ", files)
+	return files, nil
+}
+
+// extractFilenamesFromFFmpegOutput 从 ffmpeg 输出日志中提取生成的文件名
+func extractFilenamesFromFFmpegOutput(prefix string) []string {
+	pattern := fmt.Sprintf("%s_*", prefix)
+	matches, err := filepath.Glob(pattern)
+	if err != nil {
+		//log.Fatal("模式匹配错误:", err)
+		lfshook.NewLogger().Logger.Infof("=======模式匹配错误:%+v ! ============", err)
+	}
+
+	return matches
+}
+
+var Logger *log.Logger
+var LoggerDebug *log.Logger
+
+func InitLog() {
+	lfshook.NewLogger().Logger.Infof("=======模式匹配错误:%+v ! ============", configs.ConfigGlobal)
+	file, _ := os.OpenFile(configs.ConfigGlobal.RecordEventLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+	//Logger = log.New(file, "", log.LstdFlags) // 自动带时间戳
+	Logger = log.New(file, "", log.LstdFlags|log.Lmicroseconds)
+	//lfshook.NewLogger().Printf("software version: V10.01")
+
+	fileDebug, _ := os.OpenFile(configs.ConfigGlobal.DebugLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+	//Logger = log.New(file, "", log.LstdFlags) // 自动带时间戳
+	LoggerDebug = log.New(fileDebug, "", log.LstdFlags|log.Lmicroseconds|log.Llongfile)
+}
+
+func GetPadInfo(base string) (padType, padNum, connectedCab string) {
+	// 按 '/' 分割字符串
+	parts := strings.Split(base, "/")
+
+	// 取最后一段
+	lastPart := parts[len(parts)-1]
+	// 最后一段中,按‘-’分割返回前三段:parts[0], parts[1], parts[2] → PAD-2411-1411
+	ret := strings.Split(lastPart, "-")
+	if len(ret) < 3 {
+		return
+	}
+	return ret[0], ret[1], ret[2]
+}

+ 18 - 0
pkg/utils/time.go

@@ -0,0 +1,18 @@
+package utils
+
+import (
+	"strconv"
+	"strings"
+)
+
+//TimeStringToSecond 00:01:01 => 61s
+func TimeStringToSecond(input string) int {
+	times := strings.Split(input, ":")
+	timeHour := times[0]
+	timeMin := times[1]
+	timeSecond := times[2]
+	timeHourtInt, _ := strconv.Atoi(timeHour)
+	timeMinInt, _ := strconv.Atoi(timeMin)
+	timeSecondInt, _ := strconv.Atoi(timeSecond)
+	return timeHourtInt*3600 + timeMinInt*60 + timeSecondInt
+}

+ 225 - 0
pkg/utils/utils.go

@@ -0,0 +1,225 @@
+package utils
+
+import (
+	"flag"
+	"fmt"
+	"math/rand"
+	"net"
+	"os"
+	"pbx-api-gin/pkg/lfshook"
+	"strings"
+	"time"
+)
+
+// IsFlagPassed 判断是否设置了指定 flag
+func IsFlagPassed(name string) bool {
+	found := false
+	flag.Visit(func(f *flag.Flag) {
+		if f.Name == name {
+			found = true
+		}
+	})
+	return found
+}
+
+// NumberOrStringToBool 字符串 1, yes 返回 true 否则 false
+func NumberOrStringToBool(input string) bool {
+	if input == "1" {
+		return true
+	}
+	if input == "yes" {
+		return true
+	}
+	return false
+}
+
+// NumberToBool 大于0 返回 true 否则 false
+func NumberToBool(input int64) bool {
+	if input > 0 {
+		return true
+	} else {
+		return false
+	}
+}
+
+// Bool to int true 返回 1 否则 0
+func BoolToInt(input bool) int64 {
+	var output int64
+	if input {
+		output = 1
+	} else {
+		output = 0
+	}
+	return output
+}
+
+// BoolToNumber true 返回 "1" false 返回  "0"
+func BoolToNumber(input bool) string {
+	if input {
+		return "1"
+	}
+	return "0"
+}
+
+// BoolToString true 返回 "yes" false 返回  "no"
+func BoolToString(input bool) string {
+	if input {
+		return "yes"
+	}
+	return "no"
+}
+
+// YesToOn  Yes 或 yes 返回 "on" 否则返回 "off"
+// 会议 Muted 的值可能是 By admin
+func YesToOn(input string) string {
+	if input == "Yes" || input == "yes" || strings.Contains(input, "By") {
+		return "on"
+	}
+	return "off"
+}
+
+// GetRandStr 生成随机密码
+func GetRandStr(baseStr string, length int) string {
+	r := rand.New(rand.NewSource(time.Now().UnixNano() + rand.Int63()))
+	bytes := make([]byte, length)
+	l := len(baseStr)
+	for i := 0; i < length; i++ {
+		bytes[i] = baseStr[r.Intn(l)]
+	}
+	return string(bytes)
+}
+
+// SqlLike sql like 头尾添加 %%
+func SqlLike(input string) string {
+	return fmt.Sprintf("%%%s%%", input)
+}
+
+// IsChannel 判断输入是否是 Channel
+func IsChannel(input string) bool {
+	if strings.Contains(input, "SIP") {
+		return true
+	}
+
+	if strings.Contains(input, "Local") {
+		return true
+	}
+
+	if strings.Contains(input, "DAHDI") {
+		return true
+	}
+	return false
+}
+
+// 判断是否是广播分机
+func IsPAIU(input string) bool {
+	if len(input) == 4 {
+		return input[:2] == "24"
+	}
+	return false
+}
+
+// 判断是否是广播分机
+func IsPACU(input string) bool {
+	if len(input) == 4 {
+		return input[:2] == "21"
+	}
+	return false
+}
+
+// 判断是否是司机控制盒
+func IsIO(input string) bool {
+
+	if len(input) == 4 {
+		return input[:2] == "14"
+	}
+	return false
+}
+
+// 判断是否是司机控制盒
+func IsICP(input string) bool {
+
+	if len(input) == 4 {
+		return input[:2] == "23"
+	}
+	return false
+}
+
+// 根据PAD号码获取PACU号码
+func GetPacuByPad(input string) string {
+
+	if len(input) == 4 && input[:2] == "24" {
+		return fmt.Sprintf("21%c1", input[2])
+	} else {
+		return ""
+	}
+}
+
+// 判断文件是否存在
+func PathExists(path string) bool {
+	_, err := os.Stat(path)
+	if err == nil {
+		return true
+	}
+	if os.IsNotExist(err) {
+		return false
+	}
+	return false
+}
+
+func IndexOf(sliceID []string, value string) int {
+	for i, v := range sliceID {
+		if v == value {
+			return i
+		}
+	}
+	return -1
+}
+
+func CheckAsterisk() bool {
+	target := "127.0.0.1:5060"
+	count := 2
+udpDial:
+	conn, err := net.DialTimeout("udp", target, 1*time.Second)
+	if err != nil {
+		return false
+	}
+	defer conn.Close()
+
+	// 构造最小 SIP OPTIONS 请求(RFC 3261)
+	req := "OPTIONS sip:test@" + target + " SIP/2.0\r\n" +
+		"Via: SIP/2.0/UDP 127.0.0.1:5061;branch=z9hG4bK123456\r\n" +
+		"Max-Forwards: 70\r\n" +
+		"To: <sip:test@" + target + ">\r\n" +
+		"From: <sip:probe@127.0.0.1>;tag=abc123\r\n" +
+		"Call-ID: 123456789@127.0.0.1\r\n" +
+		"CSeq: 1 OPTIONS\r\n" +
+		"Contact: <sip:probe@127.0.0.1:5061>\r\n" +
+		"Accept: application/sdp\r\n" +
+		"Content-Length: 0\r\n\r\n"
+
+	_, err = conn.Write([]byte(req))
+	if err != nil {
+		time.Sleep(time.Second)
+		_, err = conn.Write([]byte(req))
+		if err != nil {
+			return false
+		}
+	}
+
+	time.Sleep(time.Millisecond * 500)
+	//conn.SetReadDeadline(time.Now().Add(1 * time.Second))
+	buf := make([]byte, 2048)
+	n, err := conn.Read(buf)
+	if err != nil || n == 0 {
+		count--
+		lfshook.NewLogger().Logger.Infof("=====CheckAsterisk response==err==count:%d ", count)
+		if count > 0 {
+			time.Sleep(time.Millisecond * 500)
+			goto udpDial
+		}
+		return false
+	}
+	resp := string(buf[:n])
+
+	return strings.HasPrefix(resp, "SIP/2.0")
+}