fix: rebuild MES as compilable go-zero+ent backend (renamed bj_power_mes), restore 3 workstation projects from pristine original, align naming; all Go projects go build clean

This commit is contained in:
SunYF
2026-08-27 10:56:41 +08:00
parent 380715f8a9
commit 371b494c54
523 changed files with 67923 additions and 24758 deletions
+28
View File
@@ -0,0 +1,28 @@
package mock
import (
"context"
"time"
)
// MockMarking implements marking.MarkingInterface with simulated marking.
type MockMarking struct {
delay time.Duration
}
// NewMockMarking creates a MockMarking with default 500ms delay.
func NewMockMarking() *MockMarking {
return &MockMarking{
delay: 500 * time.Millisecond,
}
}
// Mark simulates laser marking with a delay.
func (m *MockMarking) Mark(ctx context.Context, text string) error {
select {
case <-time.After(m.delay):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
+595
View File
@@ -0,0 +1,595 @@
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)
+155
View File
@@ -0,0 +1,155 @@
package mock
import (
"testing"
"time"
)
func TestM1050Reply(t *testing.T) {
m := NewMockPLC()
// Step 1: PlaceWorkpieceToMachine (M1040 → M1049)
m.Write("M1041", []byte{0, 7, 1, 0, 0, 0, 0, 0})
m.WriteByte("M1040", 1)
// Wait for M1049 reply
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
val, _ := m.ReadBool("M1049")
if val {
t.Log("M1049 reply received")
break
}
time.Sleep(100 * time.Millisecond)
}
// Auto-clear like waitReply does
m.Write("M1040", make([]byte, 9))
t.Log("Auto-cleared M1040")
// Simulate MachineWait
time.Sleep(1500 * time.Millisecond)
// Step 2: FetchWorkpieceFromMachine (M1050 → M1059)
m.Write("M1051", []byte{0, 7, 1, 0, 0, 0, 0, 0})
m.WriteByte("M1050", 1)
t.Log("Signal M1050 written")
// Wait for M1059 reply
deadline = time.Now().Add(2 * time.Second)
gotReply := false
for time.Now().Before(deadline) {
val, _ := m.ReadBool("M1059")
if val {
gotReply = true
t.Log("M1059 reply received - SUCCESS")
break
}
time.Sleep(100 * time.Millisecond)
}
if !gotReply {
t.Fatal("M1059 reply never received - FAILED")
}
}
// TestExchangePath 模拟换料路径:FetchWorkpieceFromTempStation(M1020) → ExchangeWorkpieceToMachine(M1060) → PlaceWorkpieceToTempStation(M1030)
func TestExchangePath(t *testing.T) {
m := NewMockPLC()
// 1. FetchWorkpieceFromTempStation (M1020 → M1029)
m.Write("M1021", []byte{1, 3, 0, 0, 0})
m.WriteByte("M1020", 1)
t.Log("M1020 written, waiting for M1029")
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
val, _ := m.ReadBool("M1029")
if val {
t.Log("M1029 reply received")
break
}
time.Sleep(100 * time.Millisecond)
}
m.Write("M1020", make([]byte, 9)) // autoClear
// 2. ExchangeWorkpieceToMachine (M1060 → M1069)
m.Write("M1061", []byte{0, 7, 1, 0, 0, 0, 0, 0})
m.WriteByte("M1060", 1)
t.Log("M1060 written, waiting for M1069")
deadline = time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
val, _ := m.ReadBool("M1069")
if val {
t.Log("M1069 reply received")
break
}
time.Sleep(100 * time.Millisecond)
}
m.Write("M1060", make([]byte, 9)) // autoClear
// 3. PlaceWorkpieceToTempStation (M1030 → M1039)
m.Write("M1031", []byte{1, 3, 0, 0, 0})
m.WriteByte("M1030", 1)
t.Log("M1030 written, waiting for M1039")
deadline = time.Now().Add(2 * time.Second)
gotReply := false
for time.Now().Before(deadline) {
val, _ := m.ReadBool("M1039")
if val {
gotReply = true
t.Log("M1039 reply received - SUCCESS")
break
}
time.Sleep(100 * time.Millisecond)
}
if !gotReply {
t.Fatal("M1039 reply never received - FAILED")
}
}
// TestConcurrentWriteByte 测试并发 WriteByte 是否会丢失 autoReply
func TestConcurrentWriteByte(t *testing.T) {
m := NewMockPLC()
// 并发写入两个信号
go func() {
m.Write("M1021", []byte{1, 3, 0, 0, 0})
m.WriteByte("M1020", 1)
}()
go func() {
m.Write("M1031", []byte{1, 3, 0, 0, 0})
m.WriteByte("M1030", 1)
}()
// 等待两个回复
deadline := time.Now().Add(2 * time.Second)
got1029 := false
got1039 := false
for time.Now().Before(deadline) {
val1029, _ := m.ReadBool("M1029")
val1039, _ := m.ReadBool("M1039")
if val1029 {
got1029 = true
}
if val1039 {
got1039 = true
}
if got1029 && got1039 {
t.Log("Both replies received - SUCCESS")
return
}
time.Sleep(100 * time.Millisecond)
}
if !got1029 {
t.Error("M1029 reply never received")
}
if !got1039 {
t.Error("M1039 reply never received")
}
}