gaoluyang
2025-10-23 fe9b7707e2c9a3e7f043ab211587ed25f1df00ef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// 模拟工业以太网/5G 网关数据接入与远程控制(前端造数)
// 设备围绕煤炭加工场景:带式输送机、破碎机、皮带秤、除尘风机、给煤机等
 
const DEFAULT_THRESHOLDS = {
  temperatureC: 85, // ℃
  pressureBar: 12, // bar
  currentA: 180, // A
  voltageV: 420, // V(三相线电压)
  powerFactor: 0.85 // cosφ
}
 
const STORAGE_KEYS = {
  thresholds: 'monitor_equipment_thresholds',
  channels: 'monitor_equipment_alarm_channels'
}
 
const DEFAULT_CHANNELS = {
  platform: true,
  sms: false,
  voice: false
}
 
const BASE_EQUIPMENTS = [
  { id: 'conv-01', name: '主煤流带式输送机#1', location: '筛分车间一线', type: 'conveyor' },
  { id: 'crusher-01', name: '齿辊破碎机#1', location: '破碎工段', type: 'crusher' },
  { id: 'feeder-01', name: '电机振动给煤机#1', location: '原煤仓下口', type: 'feeder' },
  { id: 'blower-01', name: '除尘离心风机#1', location: '装车除尘点', type: 'blower' },
  { id: 'scale-01', name: '皮带秤#1', location: '计量段', type: 'beltScale' }
]
 
function getRandomAround(base, fluct = 0.05) {
  const delta = base * fluct
  return +(base + (Math.random() * 2 - 1) * delta).toFixed(2)
}
 
function seededInitMetrics(type) {
  switch (type) {
    case 'conveyor':
      return { temperatureC: 55, pressureBar: 6.5, currentA: 95, voltageV: 400, powerFactor: 0.92 }
    case 'crusher':
      return { temperatureC: 65, pressureBar: 10.5, currentA: 140, voltageV: 405, powerFactor: 0.9 }
    case 'feeder':
      return { temperatureC: 50, pressureBar: 5.5, currentA: 60, voltageV: 398, powerFactor: 0.93 }
    case 'blower':
      return { temperatureC: 70, pressureBar: 8.2, currentA: 120, voltageV: 402, powerFactor: 0.88 }
    case 'beltScale':
      return { temperatureC: 48, pressureBar: 0.0, currentA: 35, voltageV: 401, powerFactor: 0.95 }
    default:
      return { temperatureC: 55, pressureBar: 6, currentA: 90, voltageV: 400, powerFactor: 0.9 }
  }
}
 
function nextMetrics(current, type) {
  const drift = {
    temperatureC: getRandomAround(current.temperatureC, 0.03),
    pressureBar: getRandomAround(current.pressureBar, 0.06),
    currentA: getRandomAround(current.currentA, 0.08),
    voltageV: getRandomAround(current.voltageV, 0.01),
    powerFactor: Math.max(0.6, Math.min(0.99, getRandomAround(current.powerFactor, 0.02)))
  }
  if (type === 'crusher') drift.currentA += Math.random() * 4 - 2
  if (type === 'blower') drift.powerFactor -= Math.random() * 0.01
  return {
    temperatureC: +drift.temperatureC.toFixed(2),
    pressureBar: +drift.pressureBar.toFixed(2),
    currentA: +drift.currentA.toFixed(2),
    voltageV: +drift.voltageV.toFixed(2),
    powerFactor: +drift.powerFactor.toFixed(2)
  }
}
 
export function getThresholds() {
  try {
    const raw = localStorage.getItem(STORAGE_KEYS.thresholds)
    if (!raw) return { ...DEFAULT_THRESHOLDS }
    const obj = JSON.parse(raw)
    return { ...DEFAULT_THRESHOLDS, ...obj }
  } catch (e) {
    return { ...DEFAULT_THRESHOLDS }
  }
}
 
export function saveThresholds(thresholds) {
  const merged = { ...DEFAULT_THRESHOLDS, ...thresholds }
  localStorage.setItem(STORAGE_KEYS.thresholds, JSON.stringify(merged))
  return merged
}
 
export function getAlarmChannels() {
  try {
    const raw = localStorage.getItem(STORAGE_KEYS.channels)
    if (!raw) return { ...DEFAULT_CHANNELS }
    const obj = JSON.parse(raw)
    return { ...DEFAULT_CHANNELS, ...obj }
  } catch (e) {
    return { ...DEFAULT_CHANNELS }
  }
}
 
export function saveAlarmChannels(channels) {
  const merged = { ...DEFAULT_CHANNELS, ...channels }
  localStorage.setItem(STORAGE_KEYS.channels, JSON.stringify(merged))
  return merged
}
 
// 订阅实时数据(模拟低时延、高可靠链路)
export function subscribeEquipmentData(callback, options = {}) {
  const { intervalMs = 1000, streaming = true } = options
  let isStopped = false
 
  const equipmentStates = BASE_EQUIPMENTS.map(e => ({
    ...e,
    status: 'RUNNING',
    metrics: seededInitMetrics(e.type)
  }))
 
  // 非流模式:仅推送一次快照
  if (!streaming) {
    const now = Date.now()
    equipmentStates.forEach(es => {
      callback({
        ts: now,
        equipmentId: es.id,
        name: es.name,
        location: es.location,
        status: es.status,
        metrics: { ...es.metrics }
      })
    })
    return () => {}
  }
 
  function tick() {
    if (isStopped) return
    const now = Date.now()
    equipmentStates.forEach(es => {
      if (Math.random() < 0.01) {
        es.status = es.status === 'RUNNING' ? 'STOPPED' : 'RUNNING'
      }
      if (es.status === 'RUNNING') {
        es.metrics = nextMetrics(es.metrics, es.type)
      } else {
        es.metrics = {
          ...es.metrics,
          currentA: +(es.metrics.currentA * 0.1).toFixed(2),
          powerFactor: Math.max(0.5, +(es.metrics.powerFactor * 0.8).toFixed(2))
        }
      }
      callback({
        ts: now,
        equipmentId: es.id,
        name: es.name,
        location: es.location,
        status: es.status,
        metrics: { ...es.metrics }
      })
    })
    timer = setTimeout(tick, intervalMs)
  }
 
  let timer = setTimeout(tick, intervalMs)
  return () => {
    isStopped = true
    if (timer) clearTimeout(timer)
  }
}
 
export function sendControlCommand(equipmentId, action) {
  return new Promise(resolve => {
    const delay = 50 + Math.round(Math.random() * 70)
    setTimeout(() => {
      resolve({ code: 200, msg: 'OK', data: { equipmentId, action, acceptedAt: Date.now() } })
      /* eslint-disable no-console */
      console.log(`[CONTROL] equipment=${equipmentId} action=${action}`)
    }, delay)
  })
}
 
export function detectAlarms(metrics, thresholds) {
  const over = []
  if (metrics.temperatureC > thresholds.temperatureC) over.push({ field: 'temperatureC', value: metrics.temperatureC, threshold: thresholds.temperatureC })
  if (metrics.pressureBar > thresholds.pressureBar) over.push({ field: 'pressureBar', value: metrics.pressureBar, threshold: thresholds.pressureBar })
  if (metrics.currentA > thresholds.currentA) over.push({ field: 'currentA', value: metrics.currentA, threshold: thresholds.currentA })
  if (metrics.voltageV > thresholds.voltageV) over.push({ field: 'voltageV', value: metrics.voltageV, threshold: thresholds.voltageV })
  if (metrics.powerFactor < thresholds.powerFactor) over.push({ field: 'powerFactor', value: metrics.powerFactor, threshold: thresholds.powerFactor })
  return over
}
 
export function listEquipments() {
  return BASE_EQUIPMENTS.map(e => ({ id: e.id, name: e.name, location: e.location, type: e.type }))
}
 
export default {
  subscribeEquipmentData,
  sendControlCommand,
  detectAlarms,
  getThresholds,
  saveThresholds,
  getAlarmChannels,
  saveAlarmChannels,
  listEquipments
}