综合平台编程器项目的远程存储
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

355 строки
18 KiB

  1. #include "domain/active_register_repository.h"
  2. #include "domain/register_monitor_model.h"
  3. #include "domain/register_repository.h"
  4. #include "domain/virtual_register_repository.h"
  5. #include "services/register_monitor_service.h"
  6. #include "support/test_support.h"
  7. #include <iostream>
  8. #include <cmath>
  9. #include <limits>
  10. #include <stdexcept>
  11. #include <string>
  12. #include <variant>
  13. namespace {
  14. using TestSupport::require;
  15. void testRangeManagement()
  16. {
  17. VirtualRegisterRepository repository;
  18. RegisterMonitorService service(repository);
  19. int change_count = 0;
  20. service.setAddressesChangedCallback([&change_count] { ++change_count; });
  21. const RegisterMonitorResult added = service.addRange(" d10 ", 3);
  22. require(added.succeeded && added.affectedCount == 3
  23. && service.addresses().size() == 3U,
  24. "monitor service must expand a normalized continuous address range");
  25. require(service.addresses().front() == RegisterAddress{RegisterArea::D, 10}
  26. && service.addresses().back() == RegisterAddress{RegisterArea::D, 12},
  27. "monitor service must preserve expanded address order");
  28. const RegisterMonitorResult duplicate = service.addRange("D11", 2);
  29. require(!duplicate.succeeded && duplicate.error == RegisterMonitorError::NoChange,
  30. "monitor service must reject a fully duplicated range");
  31. require(!service.addRange("M4000", 2).succeeded,
  32. "monitor service must reject a range crossing address 4000");
  33. require(change_count == 1,
  34. "failed additions must not report a monitor address change");
  35. require(service.remove({RegisterAddress{RegisterArea::D, 11}}).succeeded
  36. && service.addresses().size() == 2U,
  37. "monitor service must remove selected addresses");
  38. require(service.clear().succeeded && service.addresses().empty(),
  39. "monitor service must clear the current monitor table");
  40. require(change_count == 3,
  41. "successful add, remove and clear operations must notify address changes");
  42. }
  43. void testSharedActiveRepositoryValues()
  44. {
  45. VirtualRegisterRepository virtual_repository;
  46. VirtualRegisterRepository plc_repository;
  47. ActiveRegisterRepository active_repository(virtual_repository);
  48. RegisterMonitorService service(active_repository);
  49. require(service.addRange("M0", 1).succeeded
  50. && service.addRange("D0", 1).succeeded,
  51. "monitor service must accept one M and one D address");
  52. virtual_repository.writeBit({RegisterArea::M, 0}, true);
  53. virtual_repository.writeWord({RegisterArea::D, 0}, -123);
  54. std::vector<MonitorValue> values = service.values(false);
  55. require(values.size() == 2U && values[0].bitValue
  56. && std::get<std::int16_t>(values[1].numericValue) == -123,
  57. "monitor values must reflect the active virtual repository");
  58. plc_repository.writeBit({RegisterArea::M, 0}, false);
  59. plc_repository.writeWord({RegisterArea::D, 0}, 456);
  60. active_repository.use(plc_repository);
  61. values = service.values(false);
  62. require(!values[0].bitValue
  63. && std::get<std::int16_t>(values[1].numericValue) == 456,
  64. "monitor values must follow the active repository switch");
  65. values = service.values(true);
  66. require(values[0].state == MonitorValueState::CommunicationFault
  67. && values[1].state == MonitorValueState::CommunicationFault,
  68. "valid cached values must be marked stale during a communication fault");
  69. }
  70. void testEditingPlcMonitorAccess()
  71. {
  72. VirtualRegisterRepository virtual_repository;
  73. VirtualRegisterRepository plc_repository;
  74. ActiveRegisterRepository active_repository(virtual_repository);
  75. VirtualRegisterRepository initial_repository;
  76. RegisterMonitorService service(active_repository);
  77. service.setOfflineInitialRepository(initial_repository);
  78. service.setOfflineInitialCaptureEnabled(true);
  79. service.setEditingPlcRepository(plc_repository);
  80. service.setEditingModeActive(true);
  81. require(service.addRange("M0", 1).succeeded
  82. && service.addRange("D0", 1).succeeded,
  83. "editing PLC monitor fixture must accept M and D points");
  84. virtual_repository.writeBit({RegisterArea::M, 0}, false);
  85. virtual_repository.writeWord({RegisterArea::D, 0}, 11);
  86. plc_repository.writeBit({RegisterArea::M, 0}, true);
  87. plc_repository.writeWord({RegisterArea::D, 0}, 22);
  88. service.setEditingPlcState(true, false);
  89. std::vector<MonitorValue> values = service.values(false);
  90. require(values.size() == 2U && !values[0].bitValue
  91. && std::get<std::int16_t>(values[1].numericValue) == 11,
  92. "connecting a PLC must not automatically replace the offline editing source");
  93. const EditingMonitorSourceResult first_read_rejected =
  94. service.selectEditingSource(EditingMonitorSource::Plc);
  95. require(!first_read_rejected.succeeded
  96. && service.editingSource() == EditingMonitorSource::Offline,
  97. "editing monitor must reject PLC selection before the first read");
  98. service.setEditingPlcState(true, true);
  99. require(service.editingSource() == EditingMonitorSource::Offline
  100. && service.selectEditingSource(EditingMonitorSource::Plc).succeeded,
  101. "a completed first read must allow an explicit PLC source selection");
  102. values = service.values(false);
  103. require(values[0].state == MonitorValueState::Valid && values[0].bitValue
  104. && values[1].state == MonitorValueState::Valid
  105. && std::get<std::int16_t>(values[1].numericValue) == 22,
  106. "editing PLC monitor must display PLC cache values after first read");
  107. require(service.writeBit({RegisterArea::M, 0}, false).succeeded
  108. && service.writeNumeric(
  109. {RegisterArea::D, 0}, RegisterDataType::Int16, 33).succeeded
  110. && !plc_repository.readBit({RegisterArea::M, 0}).value
  111. && plc_repository.readWord({RegisterArea::D, 0}).value == 33
  112. && !initial_repository.readBit({RegisterArea::M, 0}).value
  113. && initial_repository.readWord({RegisterArea::D, 0}).value == 0,
  114. "editing PLC monitor writes must target PLC and skip offline capture");
  115. service.setEditingModeActive(false);
  116. values = service.values(false);
  117. require(!values[0].bitValue
  118. && std::get<std::int16_t>(values[1].numericValue) == 11,
  119. "runtime access must keep following the active repository");
  120. service.setEditingModeActive(true);
  121. service.setEditingPlcState(false, false);
  122. values = service.values(false);
  123. require(service.editingSource() == EditingMonitorSource::Plc
  124. && values[0].state == MonitorValueState::Unavailable
  125. && values[1].state == MonitorValueState::Unavailable
  126. && !service.writeBit({RegisterArea::M, 0}, true).succeeded,
  127. "a PLC disconnect must preserve the selected source and prohibit access");
  128. service.setEditingPlcState(true, true);
  129. values = service.values(false);
  130. require(service.editingSource() == EditingMonitorSource::Plc
  131. && !values[0].bitValue
  132. && std::get<std::int16_t>(values[1].numericValue) == 33,
  133. "reconnection and a new first read must resume the selected PLC source");
  134. service.setEditingPlcState(false, false);
  135. require(service.selectEditingSource(EditingMonitorSource::Offline).succeeded,
  136. "offline source must remain selectable while PLC is disconnected");
  137. values = service.values(false);
  138. require(!values[0].bitValue
  139. && std::get<std::int16_t>(values[1].numericValue) == 11,
  140. "explicit offline selection must restore virtual editing values");
  141. }
  142. void testRegisterWrites()
  143. {
  144. VirtualRegisterRepository repository;
  145. RegisterMonitorService service(repository);
  146. const RegisterAddress m0{RegisterArea::M, 0};
  147. const RegisterAddress d0{RegisterArea::D, 0};
  148. const RegisterMonitorWriteResult bit_write = service.writeBit(m0, true);
  149. require(bit_write.succeeded && repository.readBit(m0).value,
  150. "monitor service must write M values through the active repository");
  151. const RegisterMonitorWriteResult word_write = service.writeNumeric(
  152. d0, RegisterDataType::Int16, -123);
  153. require(word_write.succeeded && repository.readWord(d0).value == -123,
  154. "monitor service must write signed D values through the active repository");
  155. const RegisterMonitorWriteResult wrong_bit = service.writeBit(d0, true);
  156. require(!wrong_bit.succeeded && wrong_bit.error == RegisterError::AreaMismatch,
  157. "monitor bit writes must reject D addresses");
  158. const RegisterMonitorWriteResult wrong_word = service.writeNumeric(
  159. m0, RegisterDataType::Int16, 123);
  160. require(!wrong_word.succeeded && wrong_word.error == RegisterError::AreaMismatch,
  161. "monitor word writes must reject M addresses");
  162. require(service.writeNumeric(
  163. d0, RegisterDataType::Int16, static_cast<std::int16_t>(-32768)).succeeded
  164. && repository.readWord(d0).value == -32768
  165. && service.writeNumeric(
  166. d0, RegisterDataType::Int16, static_cast<std::int16_t>(32767)).succeeded
  167. && repository.readWord(d0).value == 32767,
  168. "monitor word writes must preserve signed 16-bit boundary values");
  169. }
  170. void testOfflineInitialValueCapture()
  171. {
  172. VirtualRegisterRepository virtual_repository;
  173. VirtualRegisterRepository initial_repository;
  174. ActiveRegisterRepository active_repository(virtual_repository);
  175. RegisterMonitorService service(active_repository);
  176. service.setOfflineInitialRepository(initial_repository);
  177. service.setOfflineInitialCaptureEnabled(true);
  178. require(service.writeBit({RegisterArea::M, 3}, true).succeeded
  179. && service.writeNumeric(
  180. {RegisterArea::D, 10}, RegisterDataType::Int16, -456).succeeded
  181. && service.writeNumeric(
  182. {RegisterArea::D, 20}, RegisterDataType::Float32, 1.5f).succeeded
  183. && service.writeNumeric(
  184. {RegisterArea::D, 30}, RegisterDataType::Int32, 305419896)
  185. .succeeded
  186. && service.writeNumeric(
  187. {RegisterArea::D, 40}, RegisterDataType::Float64, 1.0)
  188. .succeeded,
  189. "editing monitor writes must succeed against the virtual repository");
  190. const WordsReadResult captured_int32 = initial_repository.readWords(
  191. {RegisterArea::D, 30}, 2);
  192. const WordsReadResult captured_double = initial_repository.readWords(
  193. {RegisterArea::D, 40}, 4);
  194. require(initial_repository.readBit({RegisterArea::M, 3}).value
  195. && initial_repository.readWord({RegisterArea::D, 10}).value == -456
  196. && std::fabs(Float32Codec::decode(
  197. initial_repository.readWords({RegisterArea::D, 20}, 2).values[0],
  198. initial_repository.readWords({RegisterArea::D, 20}, 2).values[1])
  199. .value_or(0.0f) - 1.5f) < 0.000001f,
  200. "editing monitor writes must be captured as offline initial values");
  201. require(captured_int32.succeeded
  202. && static_cast<std::uint16_t>(captured_int32.values[0]) == 0x5678U
  203. && static_cast<std::uint16_t>(captured_int32.values[1]) == 0x1234U
  204. && captured_double.succeeded
  205. && static_cast<std::uint16_t>(captured_double.values[0]) == 0x0000U
  206. && static_cast<std::uint16_t>(captured_double.values[1]) == 0x0000U
  207. && static_cast<std::uint16_t>(captured_double.values[2]) == 0x0000U
  208. && static_cast<std::uint16_t>(captured_double.values[3]) == 0x3ff0U,
  209. "offline initial capture must keep all Int32 and Double words");
  210. service.setOfflineInitialCaptureEnabled(false);
  211. require(service.writeBit({RegisterArea::M, 3}, false).succeeded
  212. && initial_repository.readBit({RegisterArea::M, 3}).value,
  213. "runtime writes must not overwrite the captured initial values");
  214. }
  215. void testMultiWordMonitoring()
  216. {
  217. VirtualRegisterRepository repository;
  218. RegisterMonitorService service(repository);
  219. const RegisterMonitorResult added = service.addRange(
  220. "D1", 3, RegisterDataType::Float32);
  221. require(added.succeeded && added.affectedCount == 3
  222. && service.points().size() == 3U
  223. && service.points()[0].address == RegisterAddress{RegisterArea::D, 1}
  224. && service.points()[1].address == RegisterAddress{RegisterArea::D, 3}
  225. && service.points()[2].address == RegisterAddress{RegisterArea::D, 5},
  226. "Float32 monitor batches must advance by two D words");
  227. require(service.multiWordRanges().size() == 3U
  228. && service.multiWordRanges().front().wordCount == 2
  229. && service.pollAddresses().size() == 6U,
  230. "Float32 monitor points must expose both words for polling");
  231. const RegisterMonitorResult duplicate = service.addRange(
  232. "D1", 1, RegisterDataType::Float32);
  233. require(!duplicate.succeeded && duplicate.error == RegisterMonitorError::NoChange,
  234. "identical Float32 monitor points must be rejected");
  235. const RegisterMonitorResult overlap = service.addRange(
  236. "D2", 1, RegisterDataType::Int16);
  237. require(overlap.succeeded && !overlap.message.empty(),
  238. "partially overlapping monitor points must be added with a warning");
  239. require(service.remove(std::vector<MonitorPoint>{
  240. MonitorPoint{{RegisterArea::D, 2}, RegisterDataType::Int16}}).succeeded
  241. && service.points().size() == 3U,
  242. "overlapping monitor removal must identify the point type");
  243. require(service.writeNumeric(
  244. {RegisterArea::D, 1}, RegisterDataType::Float32, -2.5f).succeeded,
  245. "Float32 monitor writes must use the pair write path");
  246. std::vector<MonitorValue> values = service.values(false);
  247. require(values.front().state == MonitorValueState::Valid
  248. && std::fabs(
  249. std::get<float>(values.front().numericValue) - (-2.5f))
  250. < 0.000001f,
  251. "Float32 monitor values must decode the pair correctly");
  252. require(!service.writeNumeric(
  253. {RegisterArea::D, 4000}, RegisterDataType::Float32, 1.0f).succeeded,
  254. "Float32 monitor writes at D4000 must be rejected");
  255. const RegisterMonitorResult int32_added = service.addRange(
  256. "D10", 2, RegisterDataType::Int32);
  257. const RegisterMonitorResult double_added = service.addRange(
  258. "D20", 2, RegisterDataType::Float64);
  259. require(int32_added.succeeded && double_added.succeeded
  260. && service.points()[3].address == RegisterAddress{RegisterArea::D, 10}
  261. && service.points()[4].address == RegisterAddress{RegisterArea::D, 12}
  262. && service.points()[5].address == RegisterAddress{RegisterArea::D, 20}
  263. && service.points()[6].address == RegisterAddress{RegisterArea::D, 24}
  264. && service.pollAddresses().size() == 18U
  265. && service.multiWordRanges().size() == 7U
  266. && service.multiWordRanges()[5].wordCount == 4,
  267. "Int32 and Double monitor batches must advance by their word counts");
  268. require(service.writeNumeric(
  269. {RegisterArea::D, 10}, RegisterDataType::Int32, 305419896)
  270. .succeeded
  271. && service.writeNumeric(
  272. {RegisterArea::D, 20}, RegisterDataType::Float64, 1.25)
  273. .succeeded,
  274. "Int32 and Double monitor writes must use continuous word blocks");
  275. values = service.values(false);
  276. require(std::get<std::int32_t>(values[3].numericValue) == 0x12345678
  277. && std::get<double>(values[5].numericValue) == 1.25,
  278. "Int32 and Double monitor values must decode to their exact types");
  279. }
  280. void testMultiWordMonitoringBoundaries()
  281. {
  282. VirtualRegisterRepository repository;
  283. RegisterMonitorService boundary_service(repository);
  284. require(boundary_service.addRange(
  285. "D3999", 1, RegisterDataType::Int32).succeeded
  286. && !boundary_service.addRange(
  287. "D4000", 1, RegisterDataType::Int32).succeeded
  288. && boundary_service.addRange(
  289. "D3996", 1, RegisterDataType::Float64).succeeded
  290. && !boundary_service.addRange(
  291. "D3997", 1, RegisterDataType::Float64).succeeded
  292. && !boundary_service.addRange(
  293. "D21", 1, RegisterDataType::Float64).succeeded
  294. && !boundary_service.addRange(
  295. "M10", 1, RegisterDataType::Int32).succeeded,
  296. "multi-word monitor points must enforce D limits and Double alignment");
  297. RegisterMonitorService validated_service(repository);
  298. validated_service.setPollConfigurationValidator(
  299. [](const std::vector<RegisterAddress> &,
  300. const std::vector<RegisterWordRange> &ranges)
  301. {
  302. return ranges.empty() ? std::string{} : std::string{"轮询范围被拒绝"};
  303. });
  304. const RegisterMonitorResult rejected = validated_service.addRange(
  305. "D100", 1, RegisterDataType::Float64);
  306. require(!rejected.succeeded
  307. && rejected.error == RegisterMonitorError::PollConfigurationRejected
  308. && validated_service.points().empty(),
  309. "a rejected candidate poll configuration must not change monitor points");
  310. }
  311. } // namespace
  312. int main()
  313. {
  314. return TestSupport::runTestSuite("register monitor service tests", {
  315. {"testRangeManagement", testRangeManagement},
  316. {"testSharedActiveRepositoryValues", testSharedActiveRepositoryValues},
  317. {"testEditingPlcMonitorAccess", testEditingPlcMonitorAccess},
  318. {"testRegisterWrites", testRegisterWrites},
  319. {"testOfflineInitialValueCapture", testOfflineInitialValueCapture},
  320. {"testMultiWordMonitoring", testMultiWordMonitoring},
  321. {"testMultiWordMonitoringBoundaries", testMultiWordMonitoringBoundaries},
  322. });
  323. }