package mock import ( "encoding/binary" "fmt" "log/slog" "math" "sync" "time" goplc "bjhardman.cn/bjhardman/goplc" ) // MockPLC implements goplc.Client with in-memory storage and auto-reply. type MockPLC struct { mu sync.RWMutex boolStore map[string]bool // address string → bool value byteArrStore map[string][]byte // address string → byte array value connected bool // auto-reply rules: trigger address string → autoReply config autoReplyRules map[string]signalAutoReply // MeasureNG controls whether inspection returns NG instead of OK (default=false) MeasureNG bool } type signalAutoReply struct { replyAddr string // reply address to set true after delay delay time.Duration // delay before setting reply deviceDoneAddr string // optional: device-done signal address deviceDoneDelay time.Duration // delay before setting device-done } // NewMockPLC creates a MockPLC with default auto-reply rules. func NewMockPLC() *MockPLC { m := &MockPLC{ boolStore: make(map[string]bool), byteArrStore: make(map[string][]byte), connected: true, autoReplyRules: make(map[string]signalAutoReply), } m.initAutoReplyRules() // 清洗线就绪信号默认为 true(流水线始终就绪) m.boolStore["M1146.1"] = true // 废料台就绪信号默认为 true(Mock 环境下废料台始终就绪) m.boolStore["M1150.1"] = true return m } // initAutoReplyRules sets up auto-reply rules for all robot action signals. // Signal addresses from signal.csv: // // Task signals (PC→PLC, trigger at M1xxx, reply at M1xxx+9): // M1000 = FetchWorkpieceFromDock // M1010 = PlaceWorkpieceToDock // M1020 = FetchWorkpieceFromTempStation // M1030 = PlaceWorkpieceToTempStation // M1040 = PlaceWorkpieceToMachine // M1050 = FetchWorkpieceFromMachine // M1060 = ExchangeWorkpieceToMachine // M1070 = PlaceWorkpieceToRollingLine // M1080 = FetchWorkpieceFromRollingLine // M1090 = PlaceWorkpieceToSamplingStation // M1100 = FetchWorkpieceFromSamplingStation // M1110 = FetchWorkpieceFromMarkingStation // M1120 = MoveWorkpieceToMarkingStation // M1130 = StartScanCode // M1160 = PlaceWorkpieceToWasteStation // // Device done signals (PLC→PC, status signals): // CNCDone1 = M1140.0, CNCDone2 = M1141.0, CNCDone3 = M1142.0, CNCDone4 = M1143.0 // WasherDone = M1144.0, DeburrDone = M1145.0, CleaningDone = M1146.0 // MeasureOK = M1148.0, MeasureNG = M1148.1 // SamplingOK = M1151.0, SamplingNG = M1151.1 // ScanCodeData = DB28.DBB2 func (m *MockPLC) initAutoReplyRules() { actionDelay := 1000 * time.Millisecond // Standard robot actions: trigger at M10xx, reply at M10xx+9 simpleActions := []struct { trigger int }{ {1000}, // FetchWorkpieceFromDock {1010}, // PlaceWorkpieceToDock {1020}, // FetchWorkpieceFromTempStation {1030}, // PlaceWorkpieceToTempStation {1050}, // FetchWorkpieceFromMachine {1080}, // FetchWorkpieceFromRollingLine {1100}, // FetchWorkpieceFromSamplingStation {1110}, // FetchWorkpieceFromMarkingStation {1120}, // MoveWorkpieceToMarkingStation {1160}, // PlaceWorkpieceToWasteStation } for _, a := range simpleActions { trigger := fmt.Sprintf("M%d", a.trigger) reply := fmt.Sprintf("M%d", a.trigger+9) m.autoReplyRules[trigger] = signalAutoReply{ replyAddr: reply, delay: actionDelay, } } // PlaceWorkpieceToMachine (M1040): reply + trigger device done based on machineId m.autoReplyRules["M1040"] = signalAutoReply{ replyAddr: "M1049", delay: actionDelay, } // ExchangeWorkpieceToMachine (M1060): reply + trigger CNC done based on machineId m.autoReplyRules["M1060"] = signalAutoReply{ replyAddr: "M1069", delay: actionDelay, } // PlaceWorkpieceToRollingLine (M1070): reply + trigger MeasureOK m.autoReplyRules["M1070"] = signalAutoReply{ replyAddr: "M1079", delay: actionDelay, deviceDoneAddr: "M1148.0", // MeasureOK = M1148.0 deviceDoneDelay: 3 * time.Second, } // StartScanCode (M1130): reply + write scan code data m.autoReplyRules["M1130"] = signalAutoReply{ replyAddr: "M1139", delay: 1000 * time.Millisecond, } // PlaceWorkpieceToSamplingStation (M1090): reply + trigger SamplingOK m.autoReplyRules["M1090"] = signalAutoReply{ replyAddr: "M1099", delay: actionDelay, deviceDoneAddr: "M1151.0", // SamplingOK (s7.Address.String() omits .0) deviceDoneDelay: 2 * time.Second, } // WasherStartup (M1144.2): 启动清洗机,4s 后触发 WasherDone (M1144.0) m.autoReplyRules["M1144.2"] = signalAutoReply{ replyAddr: "M1153.2", delay: 100 * time.Millisecond, deviceDoneAddr: "M1144.0", // WasherDone deviceDoneDelay: 4 * time.Second, } } // handleTriggerSignal processes a trigger signal write and schedules auto-reply. func (m *MockPLC) handleTriggerSignal(addr string) { rule, ok := m.autoReplyRules[addr] if !ok { slog.Warn("[MockPLC] handleTriggerSignal: no rule found", "addr", addr) return } // 快照参数数据,防止并发写入覆盖(M1041/M1061 是共享参数区域) var paramSnapshot []byte if addr == "M1040" || addr == "M1060" { paramAddr := "M1041" if addr == "M1060" { paramAddr = "M1061" } m.mu.RLock() if data, exists := m.byteArrStore[paramAddr]; exists { paramSnapshot = make([]byte, len(data)) copy(paramSnapshot, data) } m.mu.RUnlock() } slog.Info("[MockPLC] handleTriggerSignal: scheduling reply", "trigger", addr, "replyAddr", rule.replyAddr, "delay", rule.delay) go func() { time.Sleep(rule.delay) m.mu.Lock() defer m.mu.Unlock() // Set reply bit to true m.boolStore[rule.replyAddr] = true slog.Info("[MockPLC] reply set", "replyAddr", rule.replyAddr, "value", true) // Auto-clear reply after 2 seconds so it doesn't stick around go func(replyAddr string) { time.Sleep(2 * time.Second) m.mu.Lock() defer m.mu.Unlock() delete(m.boolStore, replyAddr) }(rule.replyAddr) // Handle scan code: write mock scan code data if addr == "M1130" { m.writeScanCodeData() } // Handle PlaceWorkpieceToMachine (M1040): trigger device done based on machineId if addr == "M1040" { m.handlePlaceToMachineWithParams(paramSnapshot) } // Handle ExchangeWorkpieceToMachine (M1060): trigger CNC done based on machineId if addr == "M1060" { m.handleExchangeToMachineWithParams(paramSnapshot) } // Trigger device done signal if configured if rule.deviceDoneAddr != "" { doneAddr := rule.deviceDoneAddr // M1070 (PlaceWorkpieceToRollingLine): respect MeasureNG flag if addr == "M1070" && m.MeasureNG { doneAddr = "M1148.1" // MeasureNG } doneDelay := rule.deviceDoneDelay go func() { time.Sleep(doneDelay) m.mu.Lock() defer m.mu.Unlock() m.boolStore[doneAddr] = true // Auto-clear device done signal after 3 seconds go func(da string) { time.Sleep(3 * time.Second) m.mu.Lock() defer m.mu.Unlock() delete(m.boolStore, da) }(doneAddr) }() } }() } // handlePlaceToMachine reads the machineId from params written to M1041 // and triggers the corresponding device done signal. func (m *MockPLC) handlePlaceToMachine() { // Params are written via Write() to addr.Addr.Add(1,0).String() = M1041 // Format: byte0=workpieceType, byte1=machineId, byte2=location(slotNo) data, ok := m.byteArrStore["M1041"] if !ok || len(data) < 2 { return } machineId := data[1] slotNo := 1 if len(data) >= 3 { slotNo = int(data[2]) } m.triggerMachineDone(machineId, slotNo) } // handleExchangeToMachine reads the machineId from params written to M1061 func (m *MockPLC) handleExchangeToMachine() { data, ok := m.byteArrStore["M1061"] if !ok || len(data) < 2 { return } machineId := data[1] slotNo := 1 if len(data) >= 3 { slotNo = int(data[2]) } m.triggerMachineDone(machineId, slotNo) } // handlePlaceToMachineWithParams 使用快照参数触发设备完成信号(防止并发覆盖) func (m *MockPLC) handlePlaceToMachineWithParams(data []byte) { if len(data) < 2 { return } machineId := data[1] slotNo := 1 if len(data) >= 3 { slotNo = int(data[2]) } slog.Info("[MockPLC] handlePlaceToMachine", "machineId", machineId, "slotNo", slotNo) m.triggerMachineDone(machineId, slotNo) } // handleExchangeToMachineWithParams 使用快照参数触发设备完成信号(防止并发覆盖) func (m *MockPLC) handleExchangeToMachineWithParams(data []byte) { if len(data) < 2 { return } machineId := data[1] slotNo := 1 if len(data) >= 3 { slotNo = int(data[2]) } slog.Info("[MockPLC] handleExchangeToMachine", "machineId", machineId, "slotNo", slotNo) m.triggerMachineDone(machineId, slotNo) } // triggerMachineDone sets the device done signal for the given machineId after a delay. func (m *MockPLC) triggerMachineDone(machineId byte, slotNo int) { var doneAddr string var delay time.Duration switch machineId { case 1: doneAddr = "M1140.0" // CNCDone1 = M1140.0 delay = 10 * time.Second case 2: doneAddr = "M1141.0" // CNCDone2 = M1141.0 delay = 10 * time.Second case 3: doneAddr = "M1142.0" // CNCDone3 = M1142.0 delay = 10 * time.Second case 4: doneAddr = "M1143.0" // CNCDone4 = M1143.0 delay = 10 * time.Second case 5: // WASHER_HP: 不再在 PlaceWorkpieceToMachine 时触发 washerDone // 改由 WasherStartup 信号触发(见 auto-reply 规则) return case 6: doneAddr = "M1148.0" // MeasureOK (内窥镜检测) delay = 3 * time.Second case 7: doneAddr = "M1145.0" // DeburrDone = M1145.0 delay = 2 * time.Second case 8: doneAddr = "M1146.0" // CleaningDone = M1146.0 delay = 3 * time.Second case 12: doneAddr = "M1151.0" // SamplingOK delay = 3 * time.Second default: slog.Warn("[MockPLC] triggerMachineDone: unknown machineId", "machineId", machineId) return } go func() { time.Sleep(delay) m.mu.Lock() m.boolStore[doneAddr] = true m.mu.Unlock() // Auto-clear device done signal after 3 seconds go func(da string) { time.Sleep(3 * time.Second) m.mu.Lock() defer m.mu.Unlock() delete(m.boolStore, da) }(doneAddr) }() } // writeScanCodeData writes mock scan code data to ScanCodeData (DB28.DBB2). // Caller must hold m.mu.Lock. func (m *MockPLC) writeScanCodeData() { code := time.Now().Format("M20060102150405") data := make([]byte, 32) copy(data, code) m.byteArrStore["DB28.DBB2"] = data } // --- goplc.Client interface implementation --- func (m *MockPLC) Connect() error { m.mu.Lock() defer m.mu.Unlock() m.connected = true return nil } func (m *MockPLC) Close() error { m.mu.Lock() defer m.mu.Unlock() m.connected = false return nil } func (m *MockPLC) IsConnected() bool { m.mu.RLock() defer m.mu.RUnlock() return m.connected } // Read reads size bytes from the given address. func (m *MockPLC) Read(address string, size int) ([]byte, error) { m.mu.RLock() defer m.mu.RUnlock() if data, ok := m.byteArrStore[address]; ok { if len(data) >= size { return data[:size], nil } result := make([]byte, size) copy(result, data) return result, nil } return make([]byte, size), nil } // ReadBool reads a boolean value from the given address. func (m *MockPLC) ReadBool(address string) (bool, error) { m.mu.RLock() defer m.mu.RUnlock() return m.boolStore[address], nil } // ReadBools reads multiple boolean values starting from the given address. func (m *MockPLC) ReadBools(address string, size int) ([]bool, error) { m.mu.RLock() defer m.mu.RUnlock() result := make([]bool, size) for i := 0; i < size; i++ { addr := fmt.Sprintf("%s.%d", address, i) result[i] = m.boolStore[addr] } return result, nil } // ReadByte reads a single byte from the given address. func (m *MockPLC) ReadByte(address string) (byte, error) { m.mu.RLock() defer m.mu.RUnlock() if data, ok := m.byteArrStore[address]; ok && len(data) > 0 { return data[0], nil } return 0, nil } // ReadUint16 reads a 16-bit unsigned integer. func (m *MockPLC) ReadUint16(address string) (uint16, error) { b, err := m.Read(address, 2) if err != nil { return 0, err } return binary.BigEndian.Uint16(b), nil } // ReadInt16 reads a 16-bit signed integer. func (m *MockPLC) ReadInt16(address string) (int16, error) { v, err := m.ReadUint16(address) return int16(v), err } // ReadUint32 reads a 32-bit unsigned integer. func (m *MockPLC) ReadUint32(address string) (uint32, error) { b, err := m.Read(address, 4) if err != nil { return 0, err } return binary.BigEndian.Uint32(b), nil } // ReadInt32 reads a 32-bit signed integer. func (m *MockPLC) ReadInt32(address string) (int32, error) { v, err := m.ReadUint32(address) return int32(v), err } // ReadUint64 reads a 64-bit unsigned integer. func (m *MockPLC) ReadUint64(address string) (uint64, error) { b, err := m.Read(address, 8) if err != nil { return 0, err } return binary.BigEndian.Uint64(b), nil } // ReadInt64 reads a 64-bit signed integer. func (m *MockPLC) ReadInt64(address string) (int64, error) { v, err := m.ReadUint64(address) return int64(v), err } // ReadFloat32 reads a 32-bit float. func (m *MockPLC) ReadFloat32(address string) (float32, error) { b, err := m.Read(address, 4) if err != nil { return 0, err } return math.Float32frombits(binary.BigEndian.Uint32(b)), nil } // ReadFloat64 reads a 64-bit float. func (m *MockPLC) ReadFloat64(address string) (float64, error) { b, err := m.Read(address, 8) if err != nil { return 0, err } return math.Float64frombits(binary.BigEndian.Uint64(b)), nil } // Write writes byte data to the given address. func (m *MockPLC) Write(address string, data []byte) error { m.mu.Lock() m.byteArrStore[address] = make([]byte, len(data)) copy(m.byteArrStore[address], data) m.mu.Unlock() return nil } // WriteBool writes a boolean value to the given address. // If the value is true and the address matches an auto-reply rule, triggers the rule. func (m *MockPLC) WriteBool(address string, value bool) error { m.mu.Lock() m.boolStore[address] = value m.mu.Unlock() if value { m.mu.RLock() _, isTrigger := m.autoReplyRules[address] m.mu.RUnlock() if isTrigger { m.handleTriggerSignal(address) } } return nil } // WriteBools writes multiple boolean values starting from the given address. func (m *MockPLC) WriteBools(address string, values []bool) error { for i, value := range values { addr := fmt.Sprintf("%s.%d", address, i) if err := m.WriteBool(addr, value); err != nil { return err } } return nil } // WriteByte writes a single byte to the given address. // This is the key method: when a trigger signal is written (value=1), // it schedules an auto-reply. func (m *MockPLC) WriteByte(address string, value byte) error { m.mu.Lock() // Store as single-byte array m.byteArrStore[address] = []byte{value} m.mu.Unlock() // Check if this is a trigger signal (value=1) if value == 1 { m.mu.RLock() _, isTrigger := m.autoReplyRules[address] m.mu.RUnlock() slog.Info("[MockPLC] WriteByte trigger check", "address", address, "isTrigger", isTrigger) if isTrigger { m.handleTriggerSignal(address) } } return nil } // WriteUint16 writes a 16-bit unsigned integer. func (m *MockPLC) WriteUint16(address string, value uint16) error { buf := make([]byte, 2) binary.BigEndian.PutUint16(buf, value) return m.Write(address, buf) } // WriteInt16 writes a 16-bit signed integer. func (m *MockPLC) WriteInt16(address string, value int16) error { return m.WriteUint16(address, uint16(value)) } // WriteUint32 writes a 32-bit unsigned integer. func (m *MockPLC) WriteUint32(address string, value uint32) error { buf := make([]byte, 4) binary.BigEndian.PutUint32(buf, value) return m.Write(address, buf) } // WriteInt32 writes a 32-bit signed integer. func (m *MockPLC) WriteInt32(address string, value int32) error { return m.WriteUint32(address, uint32(value)) } // WriteUint64 writes a 64-bit unsigned integer. func (m *MockPLC) WriteUint64(address string, value uint64) error { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, value) return m.Write(address, buf) } // WriteInt64 writes a 64-bit signed integer. func (m *MockPLC) WriteInt64(address string, value int64) error { return m.WriteUint64(address, uint64(value)) } // WriteFloat32 writes a 32-bit float. func (m *MockPLC) WriteFloat32(address string, value float32) error { buf := make([]byte, 4) binary.BigEndian.PutUint32(buf, math.Float32bits(value)) return m.Write(address, buf) } // WriteFloat64 writes a 64-bit float. func (m *MockPLC) WriteFloat64(address string, value float64) error { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, math.Float64bits(value)) return m.Write(address, buf) } // Compile-time check that MockPLC implements goplc.Client. var _ goplc.Client = (*MockPLC)(nil)