Files
bj_power/bj_power_mes/internal/mock/plc.go
T

596 lines
16 KiB
Go

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)