model.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  1. const {
  2. formatFixedValue
  3. } = require('../../utils/number-format.js')
  4. const {
  5. clampInteger,
  6. createId,
  7. normalizeTextValue,
  8. padHex
  9. } = require('../../utils/base-utils.js')
  10. const {
  11. bytesToWords,
  12. getByteFromWord,
  13. trimTrailingNullBytes,
  14. wordsToBytes
  15. } = require('../../utils/binary-utils.js')
  16. const MAX_MODBUS_ADDRESS = 0xFFFF
  17. const MAX_GENERIC_MODBUS_ITEMS = 256
  18. const DEFAULT_TEXT_BYTE_LENGTH = 32
  19. const MAX_TEXT_BYTE_LENGTH = 32
  20. const REGISTER_TYPE_OPTIONS = [
  21. {
  22. functionCode: 0x03,
  23. key: 'holding',
  24. label: '保持寄存器',
  25. writable: true
  26. },
  27. {
  28. functionCode: 0x01,
  29. key: 'coil',
  30. label: '线圈',
  31. writable: true
  32. },
  33. {
  34. functionCode: 0x02,
  35. key: 'discrete',
  36. label: '离散输入状态',
  37. writable: false
  38. },
  39. {
  40. functionCode: 0x04,
  41. key: 'input',
  42. label: '输入寄存器',
  43. writable: false
  44. }
  45. ]
  46. const DATA_TYPE_OPTIONS = [
  47. {
  48. byteLength: 1,
  49. key: 'int8_t',
  50. label: 'int8_t',
  51. kind: 'number',
  52. wordCount: 1
  53. },
  54. {
  55. byteLength: 1,
  56. key: 'uint8_t',
  57. label: 'uint8_t',
  58. kind: 'number',
  59. wordCount: 1
  60. },
  61. {
  62. byteLength: 2,
  63. key: 'int16_t',
  64. label: 'int16_t',
  65. kind: 'number',
  66. wordCount: 1
  67. },
  68. {
  69. byteLength: 2,
  70. key: 'uint16_t',
  71. label: 'uint16_t',
  72. kind: 'number',
  73. wordCount: 1
  74. },
  75. {
  76. byteLength: 4,
  77. key: 'int32_t',
  78. label: 'int32_t',
  79. kind: 'number',
  80. wordCount: 2
  81. },
  82. {
  83. byteLength: 4,
  84. key: 'uint32_t',
  85. label: 'uint32_t',
  86. kind: 'number',
  87. wordCount: 2
  88. },
  89. {
  90. byteLength: 4,
  91. key: 'float',
  92. label: 'float',
  93. kind: 'number',
  94. wordCount: 2
  95. },
  96. {
  97. byteLength: 32,
  98. key: 'utf8',
  99. label: 'UTF-8',
  100. kind: 'text',
  101. maxByteLength: MAX_TEXT_BYTE_LENGTH,
  102. wordCount: 16
  103. },
  104. {
  105. byteLength: 32,
  106. key: 'ascii',
  107. label: 'ASCII',
  108. kind: 'text',
  109. maxByteLength: MAX_TEXT_BYTE_LENGTH,
  110. wordCount: 16
  111. },
  112. {
  113. byteLength: 2,
  114. key: 'hex',
  115. label: 'HEX',
  116. kind: 'hex',
  117. wordCount: 1
  118. }
  119. ]
  120. const DEFAULT_REGISTER_TYPE = REGISTER_TYPE_OPTIONS[0].key
  121. const DEFAULT_DATA_TYPE = 'uint16_t'
  122. const GROUP_LAYOUT_REGISTER = 'register'
  123. const GROUP_LAYOUT_STRUCT = 'struct'
  124. function normalizeAddress(value, fallback = 0) {
  125. if (typeof value === 'number') {
  126. return Number.isFinite(value) ? clampInteger(value, 0, MAX_MODBUS_ADDRESS, fallback) : fallback
  127. }
  128. const text = String(value === undefined || value === null ? '' : value).trim()
  129. if (!text) return fallback
  130. const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text
  131. if (/^[0-9A-F]+$/i.test(hexText)) {
  132. const parsedHex = parseInt(hexText, 16)
  133. return Number.isFinite(parsedHex) ? clampInteger(parsedHex, 0, MAX_MODBUS_ADDRESS, fallback) : fallback
  134. }
  135. const numberValue = Number(text)
  136. return Number.isFinite(numberValue) ? clampInteger(numberValue, 0, MAX_MODBUS_ADDRESS, fallback) : fallback
  137. }
  138. function parseConfigAddress(value) {
  139. if (typeof value === 'number') {
  140. return clampInteger(value, 0, MAX_MODBUS_ADDRESS, 0)
  141. }
  142. const text = String(value === undefined || value === null ? '' : value).trim()
  143. const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text
  144. if (!/^[0-9A-F]{1,4}$/i.test(hexText)) {
  145. throw new Error('寄存器起始地址无效')
  146. }
  147. return parseInt(hexText, 16)
  148. }
  149. function parseConfigQuantity(value, maxQuantity) {
  150. const text = String(value === undefined || value === null ? '' : value).trim()
  151. const quantity = Number(text)
  152. if (!Number.isInteger(quantity) || quantity < 1 || quantity > maxQuantity) {
  153. throw new Error(`寄存器数量需为 1 - ${maxQuantity}`)
  154. }
  155. return quantity
  156. }
  157. function getRegisterType(typeKey) {
  158. return REGISTER_TYPE_OPTIONS.find((item) => item.key === typeKey) || REGISTER_TYPE_OPTIONS[0]
  159. }
  160. function getRegisterTypeIndex(typeKey) {
  161. return Math.max(0, REGISTER_TYPE_OPTIONS.findIndex((item) => item.key === getRegisterType(typeKey).key))
  162. }
  163. function getDataType(dataType) {
  164. return DATA_TYPE_OPTIONS.find((item) => item.key === dataType)
  165. || DATA_TYPE_OPTIONS.find((item) => item.key === DEFAULT_DATA_TYPE)
  166. || DATA_TYPE_OPTIONS[0]
  167. }
  168. function getDataTypeIndex(dataType) {
  169. return Math.max(0, DATA_TYPE_OPTIONS.findIndex((item) => item.key === getDataType(dataType).key))
  170. }
  171. function normalizeTextByteLength(value, fallback = DEFAULT_TEXT_BYTE_LENGTH) {
  172. const numberValue = Number(value)
  173. const rounded = Number.isFinite(numberValue) ? Math.round(numberValue) : fallback
  174. return Math.min(Math.max(rounded, 1), MAX_TEXT_BYTE_LENGTH)
  175. }
  176. function alignEvenByteLength(byteLength) {
  177. const length = Math.max(1, Math.round(Number(byteLength) || 1))
  178. return length % 2 === 0 ? length : length + 1
  179. }
  180. function getRegisterTextByteLength(register = {}) {
  181. return normalizeTextByteLength(register.textByteLength, DEFAULT_TEXT_BYTE_LENGTH)
  182. }
  183. function isStructLayout(layout) {
  184. return layout === GROUP_LAYOUT_STRUCT
  185. }
  186. function getRegisterByteLength(dataType, register = {}) {
  187. const type = getDataType(dataType)
  188. if (type.kind === 'text') {
  189. const byteLength = getRegisterTextByteLength(register)
  190. return isStructLayout(register.layout) ? byteLength : alignEvenByteLength(byteLength)
  191. }
  192. return type.byteLength || ((type.wordCount || 1) * 2)
  193. }
  194. function getRegisterWordCount(dataType, register = {}) {
  195. return Math.max(1, Math.ceil(getRegisterByteLength(dataType, register) / 2))
  196. }
  197. function getRegisterWordCountAtOffset(dataType, byteOffset, register = {}) {
  198. const byteLength = getRegisterByteLength(dataType, register)
  199. return Math.max(1, Math.ceil((byteOffset + byteLength) / 2))
  200. }
  201. function getEncodeByteLimit(register) {
  202. return isTextRegister(register.dataType) ? getRegisterTextByteLength(register) : getRegisterByteLength(register.dataType, register)
  203. }
  204. function isTextRegister(dataType) {
  205. return getDataType(dataType).kind === 'text'
  206. }
  207. function isByteRegister(dataType) {
  208. const key = getDataType(dataType).key
  209. return key === 'int8_t' || key === 'uint8_t'
  210. }
  211. function isBitRegisterType(registerType) {
  212. return registerType === 'coil' || registerType === 'discrete'
  213. }
  214. function isHexRegister(dataType) {
  215. return getDataType(dataType).key === 'hex'
  216. }
  217. function isNumericRegister(dataType) {
  218. return getDataType(dataType).kind === 'number'
  219. }
  220. function supportsRange(dataType) {
  221. return isNumericRegister(dataType) || isHexRegister(dataType)
  222. }
  223. function supportsUnit(dataType) {
  224. return isNumericRegister(dataType)
  225. }
  226. function padWordHex(value) {
  227. return Number(value || 0).toString(16).toUpperCase().padStart(4, '0')
  228. }
  229. function formatRawWordText(words = []) {
  230. if (!Array.isArray(words) || !words.length) return '--'
  231. return words.map((word) => `0x${padWordHex(word)}`).join(' ')
  232. }
  233. function formatAddressRange(startAddress, wordCount) {
  234. const address = normalizeAddress(startAddress, 0)
  235. const count = Math.max(1, Number(wordCount) || 1)
  236. const endAddress = address + count - 1
  237. const safeEndAddress = Math.min(endAddress, MAX_MODBUS_ADDRESS)
  238. const overflowText = endAddress > MAX_MODBUS_ADDRESS ? '+' : ''
  239. if (count <= 1) return `0x${padWordHex(address)}`
  240. return `0x${padWordHex(address)}-0x${padWordHex(safeEndAddress)}${overflowText}`
  241. }
  242. function formatRegisterAddressText(address, byteOffset, byteLength, registerType) {
  243. if (isBitRegisterType(registerType)) return `0x${padHex(address)}`
  244. if (byteLength === 1) return `0x${padHex(address)}${byteOffset === 0 ? 'H' : 'L'}`
  245. return `0x${padHex(address)}`
  246. }
  247. function isAddressRangeOverflow(startAddress, wordCount) {
  248. const address = normalizeAddress(startAddress, 0)
  249. const count = Math.max(1, Number(wordCount) || 1)
  250. return address + count - 1 > MAX_MODBUS_ADDRESS
  251. }
  252. function encodeAsciiBytes(text, byteLimit = 32) {
  253. const bytes = []
  254. const stringValue = normalizeTextValue(text)
  255. for (let index = 0; index < stringValue.length; index += 1) {
  256. const code = stringValue.charCodeAt(index)
  257. if (code > 0x7F) {
  258. throw new Error('ASCII 文本只能包含 0x00 - 0x7F 字符')
  259. }
  260. bytes.push(code)
  261. if (bytes.length > byteLimit) break
  262. }
  263. if (bytes.length > byteLimit) {
  264. throw new Error(`长文本最长 ${byteLimit} 字节`)
  265. }
  266. return bytes
  267. }
  268. function encodeUtf8Bytes(text, byteLimit = 32) {
  269. const bytes = []
  270. const encoded = encodeURIComponent(normalizeTextValue(text))
  271. for (let index = 0; index < encoded.length; index += 1) {
  272. const char = encoded[index]
  273. if (char === '%') {
  274. const byte = parseInt(encoded.slice(index + 1, index + 3), 16)
  275. if (!Number.isFinite(byte)) break
  276. bytes.push(byte & 0xFF)
  277. index += 2
  278. } else {
  279. bytes.push(char.charCodeAt(0) & 0xFF)
  280. }
  281. if (bytes.length > byteLimit) break
  282. }
  283. if (bytes.length > byteLimit) {
  284. throw new Error(`长文本最长 ${byteLimit} 字节`)
  285. }
  286. return bytes
  287. }
  288. function decodeAsciiBytes(bytes = []) {
  289. return String.fromCharCode.apply(null, trimTrailingNullBytes(bytes).map((byte) => byte & 0xFF))
  290. }
  291. function decodeUtf8Bytes(bytes = []) {
  292. const trimmed = trimTrailingNullBytes(bytes)
  293. if (!trimmed.length) return ''
  294. let encoded = ''
  295. trimmed.forEach((byte) => {
  296. encoded += `%${(byte & 0xFF).toString(16).padStart(2, '0').toUpperCase()}`
  297. })
  298. try {
  299. return decodeURIComponent(encoded)
  300. } catch (error) {
  301. return decodeAsciiBytes(trimmed)
  302. }
  303. }
  304. function encodeTextBytes(text, dataType, byteLimit = MAX_TEXT_BYTE_LENGTH) {
  305. const normalizedType = getDataType(dataType).key
  306. if (normalizedType === 'ascii') return encodeAsciiBytes(text, byteLimit)
  307. return encodeUtf8Bytes(text, byteLimit)
  308. }
  309. function decodeTextBytes(bytes, dataType) {
  310. const normalizedType = getDataType(dataType).key
  311. return normalizedType === 'ascii'
  312. ? decodeAsciiBytes(bytes)
  313. : decodeUtf8Bytes(bytes)
  314. }
  315. function formatIntegerValue(value, dataType) {
  316. const type = getDataType(dataType).key
  317. const numberValue = Number(value)
  318. if (!Number.isFinite(numberValue)) return '--'
  319. if (type === 'int8_t') return String(((Math.round(numberValue) << 24) >> 24))
  320. if (type === 'uint8_t') return String(Math.round(numberValue) & 0xFF)
  321. if (type === 'int16_t') return String(((Math.round(numberValue) << 16) >> 16))
  322. if (type === 'uint16_t') return String(Math.round(numberValue) & 0xFFFF)
  323. if (type === 'int32_t') return String((Math.round(numberValue) | 0))
  324. if (type === 'uint32_t') return String(Math.round(numberValue) >>> 0)
  325. return String(Math.round(numberValue))
  326. }
  327. function formatHexValue(value) {
  328. const numberValue = Number(value)
  329. if (!Number.isFinite(numberValue)) return '--'
  330. return `0x${padWordHex(Math.round(numberValue) & 0xFFFF)}`
  331. }
  332. function formatFloatValue(value) {
  333. return formatFixedValue(value, 6).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
  334. }
  335. function parseIntegerText(value) {
  336. const text = String(value === undefined || value === null ? '' : value).trim()
  337. if (!text) return null
  338. const isHex = /^[-+]?0x[0-9a-f]+$/i.test(text) || /^0x[0-9a-f]+$/i.test(text)
  339. const parsed = isHex ? parseInt(text, 16) : Number(text)
  340. return Number.isFinite(parsed) ? parsed : null
  341. }
  342. function parseHexText(value) {
  343. const text = String(value === undefined || value === null ? '' : value).trim()
  344. if (!text) return null
  345. const hexText = text.toUpperCase().startsWith('0X') ? text.slice(2) : text
  346. if (/^[0-9A-F]{1,4}$/i.test(hexText)) {
  347. const parsedHex = parseInt(hexText, 16)
  348. return Number.isFinite(parsedHex) ? parsedHex : null
  349. }
  350. return null
  351. }
  352. function getRegisterValueTypeLabel(dataType) {
  353. return getDataType(dataType).label
  354. }
  355. function getMaxQuantity() {
  356. return MAX_GENERIC_MODBUS_ITEMS
  357. }
  358. function parseCoilValue(value) {
  359. const text = String(value === undefined || value === null ? '' : value).trim()
  360. if (!text || text === '--') return null
  361. if (['1', 'true', 'TRUE', 'on', 'ON', '开'].includes(text)) return 1
  362. if (['0', 'false', 'FALSE', 'off', 'OFF', '关'].includes(text)) return 0
  363. const coilValue = Number(text)
  364. return Number.isFinite(coilValue) ? (coilValue ? 1 : 0) : null
  365. }
  366. function getNumericRange(dataType) {
  367. const type = getDataType(dataType).key
  368. if (type === 'int8_t') return { max: 127, min: -128 }
  369. if (type === 'uint8_t') return { max: 0xFF, min: 0 }
  370. if (type === 'int16_t') return { max: 32767, min: -32768 }
  371. if (type === 'uint16_t') return { max: 0xFFFF, min: 0 }
  372. if (type === 'int32_t') return { max: 2147483647, min: -2147483648 }
  373. if (type === 'uint32_t') return { max: 0xFFFFFFFF, min: 0 }
  374. if (type === 'hex') return { max: 0xFFFF, min: 0 }
  375. return { max: Number.POSITIVE_INFINITY, min: Number.NEGATIVE_INFINITY }
  376. }
  377. function parseNumberText(value, dataType) {
  378. const text = String(value === undefined || value === null ? '' : value).trim()
  379. if (!text || text === '--') return null
  380. if (getDataType(dataType).key === 'float') {
  381. const parsed = Number(text)
  382. return Number.isFinite(parsed) ? parsed : null
  383. }
  384. if (isHexRegister(dataType)) return parseHexText(text)
  385. return parseIntegerText(text)
  386. }
  387. function parseRangeBoundary(value, dataType, label) {
  388. const text = String(value === undefined || value === null ? '' : value).trim()
  389. if (!text) return null
  390. const parsed = parseNumberText(text, dataType)
  391. if (parsed === null) {
  392. throw new Error(`${label}无效`)
  393. }
  394. return parsed
  395. }
  396. function validateNumericValue(register, value) {
  397. const dataType = getDataType(register.dataType).key
  398. const range = getNumericRange(dataType)
  399. const numberValue = Number(value)
  400. if (!Number.isFinite(numberValue)) return false
  401. if (dataType !== 'float' && Math.round(numberValue) !== numberValue) {
  402. throw new Error(`${register.name || '寄存器'} 需要整数`)
  403. }
  404. if (numberValue < range.min || numberValue > range.max) {
  405. throw new Error(`${register.name || '寄存器'} 超出 ${dataType} 范围`)
  406. }
  407. const minValue = parseRangeBoundary(register.minValue, dataType, `${register.name || '寄存器'} 最小值`)
  408. const maxValue = parseRangeBoundary(register.maxValue, dataType, `${register.name || '寄存器'} 最大值`)
  409. if (minValue !== null && numberValue < minValue) {
  410. throw new Error(`${register.name || '寄存器'} 小于限制最小值`)
  411. }
  412. if (maxValue !== null && numberValue > maxValue) {
  413. throw new Error(`${register.name || '寄存器'} 大于限制最大值`)
  414. }
  415. return true
  416. }
  417. function floatToWords(value) {
  418. const buffer = new ArrayBuffer(4)
  419. const view = new DataView(buffer)
  420. view.setFloat32(0, Number(value), false)
  421. return [view.getUint16(0, false), view.getUint16(2, false)]
  422. }
  423. function wordsToFloat(words) {
  424. if (!Array.isArray(words) || words.length < 2) return null
  425. const buffer = new ArrayBuffer(4)
  426. const view = new DataView(buffer)
  427. view.setUint16(0, Number(words[0]) & 0xFFFF, false)
  428. view.setUint16(2, Number(words[1]) & 0xFFFF, false)
  429. return view.getFloat32(0, false)
  430. }
  431. function floatToBytes(value) {
  432. const buffer = new ArrayBuffer(4)
  433. const view = new DataView(buffer)
  434. view.setFloat32(0, Number(value), false)
  435. return [
  436. view.getUint8(0),
  437. view.getUint8(1),
  438. view.getUint8(2),
  439. view.getUint8(3)
  440. ]
  441. }
  442. function bytesToFloatValue(bytes) {
  443. if (!Array.isArray(bytes) || bytes.length < 4) return null
  444. const buffer = new ArrayBuffer(4)
  445. const view = new DataView(buffer)
  446. for (let index = 0; index < 4; index += 1) {
  447. view.setUint8(index, Number(bytes[index]) & 0xFF)
  448. }
  449. return view.getFloat32(0, false)
  450. }
  451. function unsignedIntegerToBytes(value, byteLength) {
  452. let numberValue = Math.round(Number(value) || 0)
  453. const bytes = []
  454. if (numberValue < 0) {
  455. numberValue += Math.pow(2, byteLength * 8)
  456. }
  457. for (let index = byteLength - 1; index >= 0; index -= 1) {
  458. bytes[index] = numberValue & 0xFF
  459. numberValue = Math.floor(numberValue / 0x100)
  460. }
  461. return bytes
  462. }
  463. function bytesToUnsignedInteger(bytes) {
  464. return bytes.reduce((value, byte) => ((value * 0x100) + (Number(byte) & 0xFF)), 0)
  465. }
  466. function bytesToSignedInteger(bytes) {
  467. const unsignedValue = bytesToUnsignedInteger(bytes)
  468. const signLimit = Math.pow(2, bytes.length * 8 - 1)
  469. const fullRange = Math.pow(2, bytes.length * 8)
  470. return unsignedValue >= signLimit ? unsignedValue - fullRange : unsignedValue
  471. }
  472. function getRegisterDataBytes(register, words) {
  473. const dataType = getDataType(register.dataType).key
  474. const byteLength = getRegisterByteLength(dataType, register)
  475. const byteOffset = Math.max(0, Math.floor(Number(register.byteOffset) || 0))
  476. const sourceBytes = wordsToBytes(words, Math.max(0, (Array.isArray(words) ? words.length : 0) * 2))
  477. return sourceBytes.slice(byteOffset, byteOffset + byteLength)
  478. }
  479. function encodeRegisterBytes(register) {
  480. const dataType = getDataType(register.dataType).key
  481. const valueText = normalizeTextValue(register.inputValue)
  482. const byteLength = getRegisterByteLength(dataType, register)
  483. if (isTextRegister(dataType)) {
  484. const byteLimit = getEncodeByteLimit(register)
  485. const bytes = encodeTextBytes(valueText, dataType, byteLimit)
  486. const paddedBytes = bytes.slice()
  487. while (paddedBytes.length < byteLength) {
  488. paddedBytes.push(0)
  489. }
  490. return paddedBytes.slice(0, byteLength)
  491. }
  492. const numberValue = parseNumberText(valueText, dataType)
  493. if (numberValue === null) return null
  494. validateNumericValue(register, numberValue)
  495. if (dataType === 'float') return floatToBytes(numberValue)
  496. const rounded = Math.round(numberValue)
  497. if (dataType === 'int8_t' || dataType === 'uint8_t') return [rounded & 0xFF]
  498. if (dataType === 'int16_t' || dataType === 'uint16_t' || dataType === 'hex') {
  499. return unsignedIntegerToBytes(rounded, 2)
  500. }
  501. if (dataType === 'int32_t' || dataType === 'uint32_t') {
  502. return unsignedIntegerToBytes(rounded, 4)
  503. }
  504. return unsignedIntegerToBytes(rounded, byteLength)
  505. }
  506. function encodeRegisterWords(register) {
  507. const dataType = getDataType(register.dataType).key
  508. const bytes = encodeRegisterBytes(register)
  509. if (!Array.isArray(bytes)) return null
  510. if (isByteRegister(dataType)) return [bytes[0] & 0xFF]
  511. return bytesToWords(bytes.length % 2 === 0 ? bytes : bytes.concat(0))
  512. }
  513. function decodeRegisterValue(register, words) {
  514. const dataType = getDataType(register.dataType).key
  515. if (!Array.isArray(words) || words.length < getRegisterWordCount(dataType, register)) return null
  516. const bytes = getRegisterDataBytes(register, words)
  517. const byteLength = getRegisterByteLength(dataType, register)
  518. if (bytes.length < byteLength) return null
  519. if (isTextRegister(dataType)) {
  520. return decodeTextBytes(bytes.slice(0, getEncodeByteLimit(register)), dataType)
  521. }
  522. if (dataType === 'float') {
  523. return bytesToFloatValue(bytes)
  524. }
  525. if (dataType === 'int8_t') {
  526. const byteValue = bytes[0] & 0xFF
  527. return byteValue & 0x80 ? byteValue - 0x100 : byteValue
  528. }
  529. if (dataType === 'uint8_t') {
  530. return bytes[0] & 0xFF
  531. }
  532. if (dataType === 'int16_t') {
  533. return bytesToSignedInteger(bytes.slice(0, 2))
  534. }
  535. if (dataType === 'uint16_t') {
  536. return bytesToUnsignedInteger(bytes.slice(0, 2))
  537. }
  538. if (dataType === 'hex') {
  539. return bytesToUnsignedInteger(bytes.slice(0, 2))
  540. }
  541. if (dataType === 'int32_t') {
  542. return bytesToSignedInteger(bytes.slice(0, 4))
  543. }
  544. return bytesToUnsignedInteger(bytes.slice(0, 4))
  545. }
  546. function formatRegisterValue(register, rawValue) {
  547. if (rawValue === null || rawValue === undefined) return '--'
  548. const dataType = getDataType(register.dataType).key
  549. if (isTextRegister(dataType)) return normalizeTextValue(rawValue)
  550. if (dataType === 'hex') return formatHexValue(rawValue)
  551. if (dataType === 'float') return formatFloatValue(rawValue)
  552. return formatIntegerValue(rawValue, dataType)
  553. }
  554. function formatCoilDisplayValue(value) {
  555. return Number(value) ? '1' : '0'
  556. }
  557. function getRegisterSavedValue(register) {
  558. if (register.inputValue !== undefined && register.inputValue !== null) return normalizeTextValue(register.inputValue)
  559. if (register.value !== undefined && register.value !== null) return normalizeTextValue(register.value)
  560. return null
  561. }
  562. function normalizeRegisterDataType(register, registerType) {
  563. if (isBitRegisterType(registerType)) return DEFAULT_DATA_TYPE
  564. return getDataType(register.dataType || register.type || DEFAULT_DATA_TYPE).key
  565. }
  566. function normalizeRegister(register, group, index, address, byteOffset = 0) {
  567. const registerType = getRegisterType(group.registerType).key
  568. const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER
  569. const dataType = normalizeRegisterDataType(register, registerType)
  570. const textByteLength = isTextRegister(dataType)
  571. ? normalizeTextByteLength(register.textByteLength, DEFAULT_TEXT_BYTE_LENGTH)
  572. : ''
  573. const defaultValue = normalizeTextValue(register.defaultValue)
  574. const savedValue = getRegisterSavedValue(register)
  575. const inputValue = savedValue === null ? defaultValue : savedValue
  576. const rawValue = register.rawValue === undefined ? null : register.rawValue
  577. const byteLength = isBitRegisterType(registerType) ? 1 : getRegisterByteLength(dataType, { layout, textByteLength })
  578. const registerCount = isBitRegisterType(registerType) ? 1 : getRegisterWordCountAtOffset(dataType, byteOffset, { layout, textByteLength })
  579. const canShowUnit = !isBitRegisterType(registerType) && supportsUnit(dataType)
  580. const rawWords = Array.isArray(register.rawWords)
  581. ? register.rawWords.slice(0, registerCount).map((word) => Number(word) & 0xFFFF)
  582. : []
  583. const rawValueText = rawValue === null
  584. ? '--'
  585. : (isBitRegisterType(registerType) ? formatCoilDisplayValue(rawValue) : formatRawWordText(rawWords))
  586. const displayValue = rawValue === null
  587. ? (inputValue.trim() ? inputValue : '--')
  588. : formatRegisterValue({ ...register, dataType, byteOffset }, rawValue)
  589. return {
  590. address,
  591. addressRangeText: isBitRegisterType(registerType)
  592. ? `0x${padHex(address)}`
  593. : formatAddressRange(address, registerCount),
  594. addressText: formatRegisterAddressText(address, byteOffset, byteLength, registerType),
  595. byteLength,
  596. byteLengthText: isBitRegisterType(registerType)
  597. ? '1bit'
  598. : (textByteLength && textByteLength !== byteLength ? `${textByteLength}B/占${byteLength}B` : `${byteLength}B`),
  599. byteStart: Math.max(0, Math.floor(Number(register.byteStart) || 0)),
  600. dataType,
  601. dataTypeIndex: getDataTypeIndex(dataType),
  602. dataTypeText: getRegisterValueTypeLabel(dataType),
  603. defaultValue,
  604. displayValue,
  605. id: register.id || createId('gm-reg'),
  606. inputType: isTextRegister(dataType) ? 'text' : 'text',
  607. inputValue,
  608. isStructField: layout === GROUP_LAYOUT_STRUCT || !!register.isStructField,
  609. layout,
  610. isDirty: !!register.isDirty,
  611. maxValue: normalizeTextValue(register.maxValue),
  612. minValue: normalizeTextValue(register.minValue),
  613. name: register.name || `寄存器 ${index + 1}`,
  614. rawValue,
  615. rawValueText,
  616. rawWords,
  617. registerCount,
  618. byteOffset,
  619. registerType,
  620. showDataType: !isBitRegisterType(registerType),
  621. showRange: !isBitRegisterType(registerType) && supportsRange(dataType),
  622. showTextLength: !isBitRegisterType(registerType) && isTextRegister(dataType),
  623. showUnit: canShowUnit,
  624. textByteLength,
  625. unit: canShowUnit ? normalizeTextValue(register.unit).trim() : '',
  626. remark: register.remark || ''
  627. }
  628. }
  629. function normalizeGroup(group) {
  630. const registerType = getRegisterType(group.registerType || group.type || DEFAULT_REGISTER_TYPE)
  631. const startAddress = normalizeAddress(group.startAddress, 0)
  632. const maxQuantity = getMaxQuantity(registerType.key)
  633. const sourceRegisters = Array.isArray(group.registers) ? group.registers : []
  634. const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER
  635. const hasExplicitQuantity = group.quantity !== undefined && group.quantity !== null && group.quantity !== ''
  636. const quantity = hasExplicitQuantity
  637. ? clampInteger(group.quantity, 1, maxQuantity, 1)
  638. : clampInteger(sourceRegisters.length || 1, 1, maxQuantity, 1)
  639. const baseGroup = {
  640. deleteVisible: !!group.deleteVisible,
  641. expanded: group.expanded === true,
  642. id: group.id || createId('gm-group'),
  643. isStructLayout: layout === GROUP_LAYOUT_STRUCT,
  644. layout,
  645. name: String(group.name || group.groupName || '寄存器组').trim() || '寄存器组',
  646. quantity,
  647. registerType: registerType.key,
  648. startAddress,
  649. touchStartX: 0
  650. }
  651. const registers = []
  652. let nextAddress = startAddress
  653. let nextByteOffset = 0
  654. for (let index = 0; index < quantity; index += 1) {
  655. const sourceRegister = sourceRegisters[index] || {}
  656. let normalizedSourceRegister = sourceRegister
  657. const dataType = normalizeRegisterDataType(sourceRegister, baseGroup.registerType)
  658. const textByteLength = isTextRegister(dataType)
  659. ? normalizeTextByteLength(sourceRegister.textByteLength, DEFAULT_TEXT_BYTE_LENGTH)
  660. : ''
  661. const isBitRegister = isBitRegisterType(baseGroup.registerType)
  662. let address = startAddress + index
  663. let byteOffset = 0
  664. if (!isBitRegister) {
  665. const byteLength = getRegisterByteLength(dataType, { layout, textByteLength })
  666. if (layout !== GROUP_LAYOUT_STRUCT && !isByteRegister(dataType) && nextByteOffset % 2 !== 0) {
  667. nextByteOffset += 1
  668. }
  669. address = startAddress + Math.floor(nextByteOffset / 2)
  670. byteOffset = nextByteOffset % 2
  671. normalizedSourceRegister = {
  672. ...sourceRegister,
  673. byteStart: nextByteOffset
  674. }
  675. nextByteOffset += byteLength
  676. }
  677. const register = normalizeRegister(normalizedSourceRegister, baseGroup, index, address, byteOffset)
  678. registers.push(register)
  679. if (isBitRegister) nextAddress += register.registerCount
  680. }
  681. const byteLength = isBitRegisterType(baseGroup.registerType)
  682. ? Math.max(1, nextAddress - startAddress)
  683. : Math.max(1, nextByteOffset)
  684. const paddedByteLength = isBitRegisterType(baseGroup.registerType)
  685. ? byteLength
  686. : alignEvenByteLength(byteLength)
  687. const wordQuantity = isBitRegisterType(baseGroup.registerType)
  688. ? Math.max(1, nextAddress - startAddress)
  689. : Math.max(1, paddedByteLength / 2)
  690. const addressOverflow = isAddressRangeOverflow(startAddress, wordQuantity)
  691. const endAddress = startAddress + wordQuantity - 1
  692. return {
  693. ...baseGroup,
  694. addressRangeText: formatAddressRange(startAddress, wordQuantity),
  695. addressOverflow,
  696. addressWarningText: addressOverflow ? '地址超出 0xFFFF' : '',
  697. endAddressText: addressOverflow ? `0x${padHex(MAX_MODBUS_ADDRESS)}+` : `0x${padHex(endAddress)}`,
  698. functionCode: registerType.functionCode,
  699. isReadOnly: !registerType.writable,
  700. byteLength,
  701. maxQuantity,
  702. registerTypeIndex: getRegisterTypeIndex(registerType.key),
  703. registerTypeText: registerType.label,
  704. registers,
  705. paddedByteLength,
  706. startAddressText: `0x${padHex(startAddress)}`,
  707. wordQuantity,
  708. writable: registerType.writable
  709. }
  710. }
  711. function normalizeGroupConfig(config = {}) {
  712. const registerType = config.registerTypeIndex !== undefined && config.registerTypeIndex !== null
  713. ? (REGISTER_TYPE_OPTIONS[Number(config.registerTypeIndex)] || REGISTER_TYPE_OPTIONS[0])
  714. : getRegisterType(config.registerType || config.type || DEFAULT_REGISTER_TYPE)
  715. const maxQuantity = getMaxQuantity(registerType.key)
  716. return {
  717. layout: isStructLayout(config.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER,
  718. name: String(config.name || config.groupName || '寄存器组').trim() || '寄存器组',
  719. quantity: parseConfigQuantity(config.quantity, maxQuantity),
  720. registerType: registerType.key,
  721. startAddress: parseConfigAddress(config.startAddress)
  722. }
  723. }
  724. function getRegisterJsonValue(register) {
  725. if (register.inputValue !== undefined && register.inputValue !== null) {
  726. return normalizeTextValue(register.inputValue)
  727. }
  728. if (register.defaultValue !== undefined && register.defaultValue !== null) {
  729. return normalizeTextValue(register.defaultValue)
  730. }
  731. if (register.displayValue !== undefined && register.displayValue !== null && register.displayValue !== '--') {
  732. return normalizeTextValue(register.displayValue)
  733. }
  734. return ''
  735. }
  736. function normalizeImportedRegisterDataType(register) {
  737. const dataType = register.dataType || register.type || DEFAULT_DATA_TYPE
  738. return getDataType(dataType).key
  739. }
  740. function cloneImportedGroup(group) {
  741. return {
  742. layout: isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER,
  743. name: group.name,
  744. quantity: group.quantity,
  745. registerType: group.registerType || group.type || DEFAULT_REGISTER_TYPE,
  746. registers: (Array.isArray(group.registers) ? group.registers : []).map((register) => ({
  747. dataType: normalizeImportedRegisterDataType(register),
  748. defaultValue: register.defaultValue,
  749. inputValue: register.inputValue,
  750. maxValue: register.maxValue,
  751. minValue: register.minValue,
  752. name: register.name,
  753. isStructField: !!register.isStructField,
  754. textByteLength: register.textByteLength,
  755. remark: register.remark,
  756. unit: register.unit,
  757. value: register.value
  758. })),
  759. startAddress: group.startAddress
  760. }
  761. }
  762. function splitWordSpans(startAddress, quantity, maxQuantity) {
  763. const spans = []
  764. let address = normalizeAddress(startAddress, 0)
  765. let remaining = Math.max(0, Math.floor(Number(quantity) || 0))
  766. while (remaining > 0) {
  767. const spanQuantity = Math.min(remaining, maxQuantity)
  768. spans.push({
  769. address,
  770. quantity: spanQuantity
  771. })
  772. address += spanQuantity
  773. remaining -= spanQuantity
  774. }
  775. return spans
  776. }
  777. function getRegisterWriteValueText(register) {
  778. if (register.inputValue !== undefined && register.inputValue !== null) return normalizeTextValue(register.inputValue)
  779. if (register.defaultValue !== undefined && register.defaultValue !== null) return normalizeTextValue(register.defaultValue)
  780. return ''
  781. }
  782. function getRegisterWordsFromWordCache(register, wordCache) {
  783. const words = []
  784. for (let offset = 0; offset < register.registerCount; offset += 1) {
  785. const word = wordCache[register.address + offset]
  786. if (word === undefined) return null
  787. words.push(word)
  788. }
  789. return words
  790. }
  791. function decodeRegisterFromWordCache(register, wordCache) {
  792. const words = getRegisterWordsFromWordCache(register, wordCache)
  793. if (!words) return null
  794. return decodeRegisterValue(register, words)
  795. }
  796. function getRegisterEncodedWords(register) {
  797. return encodeRegisterWords({
  798. ...register,
  799. inputValue: getRegisterWriteValueText(register)
  800. })
  801. }
  802. function getRegisterByteStart(register, groupStartAddress = 0) {
  803. if (Number.isFinite(Number(register.byteStart))) {
  804. return Math.max(0, Math.floor(Number(register.byteStart)))
  805. }
  806. return Math.max(0, ((Number(register.address) || 0) - (Number(groupStartAddress) || 0)) * 2 + (Number(register.byteOffset) || 0))
  807. }
  808. function getGroupEncodedWords(group) {
  809. const byteLength = Math.max(2, Number(group && group.paddedByteLength) || ((Number(group && group.wordQuantity) || 1) * 2))
  810. const bytes = Array.from({ length: byteLength }, () => 0)
  811. const registers = Array.isArray(group && group.registers) ? group.registers : []
  812. registers.forEach((register) => {
  813. const registerBytes = encodeRegisterBytes(register)
  814. if (!Array.isArray(registerBytes) || !registerBytes.length) {
  815. throw new Error(`${register.name || '寄存器'} 没有有效写入值`)
  816. }
  817. const byteStart = getRegisterByteStart(register, group.startAddress)
  818. for (let offset = 0; offset < register.byteLength; offset += 1) {
  819. if (byteStart + offset < bytes.length) {
  820. bytes[byteStart + offset] = Number(registerBytes[offset] || 0) & 0xFF
  821. }
  822. }
  823. })
  824. return bytesToWords(bytes)
  825. }
  826. function validateRegisterValue(register, value) {
  827. const valueText = normalizeTextValue(
  828. value === undefined || value === null ? getRegisterWriteValueText(register) : value
  829. ).trim()
  830. if (!valueText || valueText === '--') return true
  831. if (registerTypeIsBit(register)) {
  832. if (parseCoilValue(valueText) === null) {
  833. throw new Error(`${register.name || '线圈'} 只能填写 0 或 1`)
  834. }
  835. return true
  836. }
  837. const dataType = getDataType(register.dataType).key
  838. if (isTextRegister(dataType)) {
  839. encodeTextBytes(valueText, dataType, getEncodeByteLimit(register))
  840. return true
  841. }
  842. const numberValue = parseNumberText(valueText, dataType)
  843. if (numberValue === null) {
  844. throw new Error(`${register.name || '寄存器'} 输入值无效`)
  845. }
  846. return validateNumericValue(register, numberValue)
  847. }
  848. function registerTypeIsBit(register) {
  849. return !!register && isBitRegisterType(register.registerType)
  850. }
  851. module.exports = {
  852. DATA_TYPE_OPTIONS,
  853. DEFAULT_DATA_TYPE,
  854. DEFAULT_REGISTER_TYPE,
  855. GROUP_LAYOUT_REGISTER,
  856. GROUP_LAYOUT_STRUCT,
  857. MAX_MODBUS_ADDRESS,
  858. REGISTER_TYPE_OPTIONS,
  859. cloneImportedGroup,
  860. decodeRegisterFromWordCache,
  861. decodeRegisterValue,
  862. formatCoilDisplayValue,
  863. formatRegisterValue,
  864. getDataType,
  865. getRegisterEncodedWords,
  866. getGroupEncodedWords,
  867. getRegisterWordCount,
  868. getRegisterJsonValue,
  869. getRegisterWordsFromWordCache,
  870. getRegisterWriteValueText,
  871. isAddressRangeOverflow,
  872. isBitRegisterType,
  873. isByteRegister,
  874. isTextRegister,
  875. normalizeGroup,
  876. normalizeGroupConfig,
  877. normalizeRegister,
  878. parseCoilValue,
  879. registerTypeIsBit,
  880. splitWordSpans,
  881. validateRegisterValue
  882. }