aslmp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. aslmp-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +192 -0
  2. aslmp-0.1.0/.github/ISSUE_TEMPLATE/config.yml +32 -0
  3. aslmp-0.1.0/.github/ISSUE_TEMPLATE/cpu_behaviour.yml +159 -0
  4. aslmp-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +63 -0
  5. aslmp-0.1.0/.github/dependabot.yml +25 -0
  6. aslmp-0.1.0/.github/workflows/ci.yml +111 -0
  7. aslmp-0.1.0/.github/workflows/publish.yml +94 -0
  8. aslmp-0.1.0/.gitignore +38 -0
  9. aslmp-0.1.0/CHANGELOG.md +575 -0
  10. aslmp-0.1.0/CODE_OF_CONDUCT.md +132 -0
  11. aslmp-0.1.0/CONTRIBUTING.md +349 -0
  12. aslmp-0.1.0/LICENSE +202 -0
  13. aslmp-0.1.0/NOTICE +26 -0
  14. aslmp-0.1.0/PKG-INFO +892 -0
  15. aslmp-0.1.0/README.md +859 -0
  16. aslmp-0.1.0/SECURITY.md +189 -0
  17. aslmp-0.1.0/bench/_report.py +136 -0
  18. aslmp-0.1.0/bench/access_patterns.py +127 -0
  19. aslmp-0.1.0/bench/soak.py +727 -0
  20. aslmp-0.1.0/bench/transports.py +158 -0
  21. aslmp-0.1.0/docs/architecture.md +152 -0
  22. aslmp-0.1.0/docs/benchmarking.md +220 -0
  23. aslmp-0.1.0/docs/cli.md +219 -0
  24. aslmp-0.1.0/docs/errors.md +124 -0
  25. aslmp-0.1.0/docs/hardware.md +884 -0
  26. aslmp-0.1.0/docs/unverified.md +286 -0
  27. aslmp-0.1.0/pyproject.toml +112 -0
  28. aslmp-0.1.0/src/aslmp/__init__.py +955 -0
  29. aslmp-0.1.0/src/aslmp/__main__.py +21 -0
  30. aslmp-0.1.0/src/aslmp/_clock.py +37 -0
  31. aslmp-0.1.0/src/aslmp/_version.py +9 -0
  32. aslmp-0.1.0/src/aslmp/blocks/__init__.py +91 -0
  33. aslmp-0.1.0/src/aslmp/blocks/fields.py +911 -0
  34. aslmp-0.1.0/src/aslmp/blocks/layout.py +624 -0
  35. aslmp-0.1.0/src/aslmp/blocks/plan.py +1587 -0
  36. aslmp-0.1.0/src/aslmp/client.py +2623 -0
  37. aslmp-0.1.0/src/aslmp/commands/__init__.py +122 -0
  38. aslmp-0.1.0/src/aslmp/commands/base.py +924 -0
  39. aslmp-0.1.0/src/aslmp/commands/batch.py +390 -0
  40. aslmp-0.1.0/src/aslmp/commands/block.py +351 -0
  41. aslmp-0.1.0/src/aslmp/commands/info.py +376 -0
  42. aslmp-0.1.0/src/aslmp/commands/monitor.py +290 -0
  43. aslmp-0.1.0/src/aslmp/commands/ondemand.py +152 -0
  44. aslmp-0.1.0/src/aslmp/commands/password.py +165 -0
  45. aslmp-0.1.0/src/aslmp/commands/random.py +810 -0
  46. aslmp-0.1.0/src/aslmp/commands/registry.py +205 -0
  47. aslmp-0.1.0/src/aslmp/commands/remote.py +455 -0
  48. aslmp-0.1.0/src/aslmp/connection.py +1097 -0
  49. aslmp-0.1.0/src/aslmp/data/__init__.py +215 -0
  50. aslmp-0.1.0/src/aslmp/data/ambiguities.tsv +54 -0
  51. aslmp-0.1.0/src/aslmp/data/devices.tsv +62 -0
  52. aslmp-0.1.0/src/aslmp/data/end_codes.tsv +126 -0
  53. aslmp-0.1.0/src/aslmp/data/limits.tsv +138 -0
  54. aslmp-0.1.0/src/aslmp/data/manuals.tsv +22 -0
  55. aslmp-0.1.0/src/aslmp/data/model_codes.tsv +105 -0
  56. aslmp-0.1.0/src/aslmp/data/ranges_iqf.tsv +181 -0
  57. aslmp-0.1.0/src/aslmp/data/ranges_iqr.tsv +91 -0
  58. aslmp-0.1.0/src/aslmp/entries.py +237 -0
  59. aslmp-0.1.0/src/aslmp/errors/__init__.py +1380 -0
  60. aslmp-0.1.0/src/aslmp/errors/endcodes.py +2130 -0
  61. aslmp-0.1.0/src/aslmp/errors/routing.py +468 -0
  62. aslmp-0.1.0/src/aslmp/health.py +437 -0
  63. aslmp-0.1.0/src/aslmp/identity.py +190 -0
  64. aslmp-0.1.0/src/aslmp/loop.py +343 -0
  65. aslmp-0.1.0/src/aslmp/observability.py +951 -0
  66. aslmp-0.1.0/src/aslmp/profile.py +1311 -0
  67. aslmp-0.1.0/src/aslmp/profiles/__init__.py +179 -0
  68. aslmp-0.1.0/src/aslmp/profiles/fx5s.py +46 -0
  69. aslmp-0.1.0/src/aslmp/profiles/fx5u.py +912 -0
  70. aslmp-0.1.0/src/aslmp/profiles/fx5uc.py +44 -0
  71. aslmp-0.1.0/src/aslmp/profiles/fx5uj.py +87 -0
  72. aslmp-0.1.0/src/aslmp/profiles/iq_r.py +572 -0
  73. aslmp-0.1.0/src/aslmp/profiles/l.py +38 -0
  74. aslmp-0.1.0/src/aslmp/profiles/q.py +239 -0
  75. aslmp-0.1.0/src/aslmp/py.typed +0 -0
  76. aslmp-0.1.0/src/aslmp/resilience.py +569 -0
  77. aslmp-0.1.0/src/aslmp/results.py +362 -0
  78. aslmp-0.1.0/src/aslmp/sync.py +1026 -0
  79. aslmp-0.1.0/src/aslmp/testing/__init__.py +167 -0
  80. aslmp-0.1.0/src/aslmp/testing/conformance.py +855 -0
  81. aslmp-0.1.0/src/aslmp/testing/dispatch.py +1204 -0
  82. aslmp-0.1.0/src/aslmp/testing/memory.py +492 -0
  83. aslmp-0.1.0/src/aslmp/testing/pathology.py +401 -0
  84. aslmp-0.1.0/src/aslmp/testing/pytest_plugin.py +111 -0
  85. aslmp-0.1.0/src/aslmp/testing/scenario.py +142 -0
  86. aslmp-0.1.0/src/aslmp/testing/server.py +1075 -0
  87. aslmp-0.1.0/src/aslmp/testing/targets.py +711 -0
  88. aslmp-0.1.0/src/aslmp/testing/vectors.py +225 -0
  89. aslmp-0.1.0/src/aslmp/timed.py +788 -0
  90. aslmp-0.1.0/src/aslmp/timing.py +605 -0
  91. aslmp-0.1.0/src/aslmp/tools/__init__.py +82 -0
  92. aslmp-0.1.0/src/aslmp/tools/__main__.py +103 -0
  93. aslmp-0.1.0/src/aslmp/tools/_common.py +307 -0
  94. aslmp-0.1.0/src/aslmp/tools/ambiguities.py +255 -0
  95. aslmp-0.1.0/src/aslmp/tools/bench.py +521 -0
  96. aslmp-0.1.0/src/aslmp/tools/capabilities.py +150 -0
  97. aslmp-0.1.0/src/aslmp/tools/cite.py +175 -0
  98. aslmp-0.1.0/src/aslmp/tools/identify.py +100 -0
  99. aslmp-0.1.0/src/aslmp/tools/probe.py +134 -0
  100. aslmp-0.1.0/src/aslmp/tools/proxy.py +313 -0
  101. aslmp-0.1.0/src/aslmp/tools/read.py +258 -0
  102. aslmp-0.1.0/src/aslmp/tools/serve.py +188 -0
  103. aslmp-0.1.0/src/aslmp/tools/verify_ranges.py +266 -0
  104. aslmp-0.1.0/src/aslmp/tools/write.py +252 -0
  105. aslmp-0.1.0/src/aslmp/transport/__init__.py +65 -0
  106. aslmp-0.1.0/src/aslmp/transport/base.py +451 -0
  107. aslmp-0.1.0/src/aslmp/transport/inflight.py +292 -0
  108. aslmp-0.1.0/src/aslmp/transport/tcp.py +562 -0
  109. aslmp-0.1.0/src/aslmp/transport/udp.py +701 -0
  110. aslmp-0.1.0/src/aslmp/wire/__init__.py +13 -0
  111. aslmp-0.1.0/src/aslmp/wire/address.py +666 -0
  112. aslmp-0.1.0/src/aslmp/wire/citations.py +268 -0
  113. aslmp-0.1.0/src/aslmp/wire/codec.py +853 -0
  114. aslmp-0.1.0/src/aslmp/wire/devicetable.py +1195 -0
  115. aslmp-0.1.0/src/aslmp/wire/devspec.py +287 -0
  116. aslmp-0.1.0/src/aslmp/wire/frames.py +777 -0
  117. aslmp-0.1.0/src/aslmp/wire/raw.py +482 -0
  118. aslmp-0.1.0/src/aslmp/wire/reader.py +232 -0
  119. aslmp-0.1.0/src/aslmp/wire/route.py +195 -0
  120. aslmp-0.1.0/src/aslmp/wire/subcommand.py +144 -0
  121. aslmp-0.1.0/tests/hardware/test_fx5u.py +1013 -0
  122. aslmp-0.1.0/tests/hardware/test_remote_control.py +530 -0
  123. aslmp-0.1.0/tests/integration/test_blocks_against_simulator.py +469 -0
  124. aslmp-0.1.0/tests/integration/test_client_against_simulator.py +960 -0
  125. aslmp-0.1.0/tests/integration/test_conformance.py +605 -0
  126. aslmp-0.1.0/tests/typing/consumer.py +201 -0
  127. aslmp-0.1.0/tests/unit/test_address.py +489 -0
  128. aslmp-0.1.0/tests/unit/test_blocks_bounds.py +695 -0
  129. aslmp-0.1.0/tests/unit/test_blocks_fields.py +374 -0
  130. aslmp-0.1.0/tests/unit/test_blocks_layout.py +411 -0
  131. aslmp-0.1.0/tests/unit/test_blocks_plan.py +848 -0
  132. aslmp-0.1.0/tests/unit/test_citations.py +1628 -0
  133. aslmp-0.1.0/tests/unit/test_client.py +966 -0
  134. aslmp-0.1.0/tests/unit/test_codec.py +720 -0
  135. aslmp-0.1.0/tests/unit/test_commands_base.py +319 -0
  136. aslmp-0.1.0/tests/unit/test_commands_identity.py +196 -0
  137. aslmp-0.1.0/tests/unit/test_commands_random.py +368 -0
  138. aslmp-0.1.0/tests/unit/test_commands_refusals.py +626 -0
  139. aslmp-0.1.0/tests/unit/test_commands_registry.py +242 -0
  140. aslmp-0.1.0/tests/unit/test_commands_vectors.py +575 -0
  141. aslmp-0.1.0/tests/unit/test_connection.py +879 -0
  142. aslmp-0.1.0/tests/unit/test_devicetable.py +441 -0
  143. aslmp-0.1.0/tests/unit/test_devspec.py +413 -0
  144. aslmp-0.1.0/tests/unit/test_entries.py +268 -0
  145. aslmp-0.1.0/tests/unit/test_errors.py +867 -0
  146. aslmp-0.1.0/tests/unit/test_frames.py +760 -0
  147. aslmp-0.1.0/tests/unit/test_generated_is_current.py +241 -0
  148. aslmp-0.1.0/tests/unit/test_health.py +488 -0
  149. aslmp-0.1.0/tests/unit/test_inflight.py +240 -0
  150. aslmp-0.1.0/tests/unit/test_layering.py +659 -0
  151. aslmp-0.1.0/tests/unit/test_loop.py +338 -0
  152. aslmp-0.1.0/tests/unit/test_observability.py +791 -0
  153. aslmp-0.1.0/tests/unit/test_profile.py +588 -0
  154. aslmp-0.1.0/tests/unit/test_profiles.py +542 -0
  155. aslmp-0.1.0/tests/unit/test_public_surface.py +651 -0
  156. aslmp-0.1.0/tests/unit/test_read_write_symmetry.py +658 -0
  157. aslmp-0.1.0/tests/unit/test_reader.py +324 -0
  158. aslmp-0.1.0/tests/unit/test_resilience.py +353 -0
  159. aslmp-0.1.0/tests/unit/test_results.py +262 -0
  160. aslmp-0.1.0/tests/unit/test_simulator_dispatch.py +885 -0
  161. aslmp-0.1.0/tests/unit/test_simulator_lifecycle.py +94 -0
  162. aslmp-0.1.0/tests/unit/test_simulator_memory.py +213 -0
  163. aslmp-0.1.0/tests/unit/test_simulator_pathology.py +678 -0
  164. aslmp-0.1.0/tests/unit/test_simulator_scenario.py +100 -0
  165. aslmp-0.1.0/tests/unit/test_simulator_state.py +414 -0
  166. aslmp-0.1.0/tests/unit/test_simulator_switchboard.py +994 -0
  167. aslmp-0.1.0/tests/unit/test_simulator_targets.py +225 -0
  168. aslmp-0.1.0/tests/unit/test_simulator_vectors.py +244 -0
  169. aslmp-0.1.0/tests/unit/test_subcommand.py +142 -0
  170. aslmp-0.1.0/tests/unit/test_suite_hygiene.py +304 -0
  171. aslmp-0.1.0/tests/unit/test_sync.py +380 -0
  172. aslmp-0.1.0/tests/unit/test_timing.py +573 -0
  173. aslmp-0.1.0/tests/unit/test_tools.py +1000 -0
  174. aslmp-0.1.0/tests/unit/test_transport_base.py +192 -0
  175. aslmp-0.1.0/tests/unit/test_transport_tcp.py +782 -0
  176. aslmp-0.1.0/tests/unit/test_transport_udp.py +654 -0
  177. aslmp-0.1.0/tests/unit/test_wheel_contents.py +287 -0
  178. aslmp-0.1.0/tests/vectors/codec_vectors.jsonl +50 -0
  179. aslmp-0.1.0/tests/vectors/command_vectors.jsonl +33 -0
  180. aslmp-0.1.0/tests/vectors/frame_vectors.jsonl +39 -0
  181. aslmp-0.1.0/tools/gen_devicetable.py +294 -0
  182. aslmp-0.1.0/tools/gen_endcodes.py +255 -0
  183. aslmp-0.1.0/tools/gen_timed.py +508 -0
@@ -0,0 +1,192 @@
1
+ name: Bug report
2
+ description: Something in aslmp behaves differently from what it says it does.
3
+ title: "[bug] "
4
+ labels: ["bug"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ **Before anything else.** If your CPU is attached to something that can move,
10
+ do not reproduce a bug on it. This library writes to PLCs.
11
+
12
+ Two reports that belong somewhere else:
13
+
14
+ - *"My CPU answers differently from your table."* That is not a bug, it is the
15
+ most valuable report this project receives — use the **CPU behaviour
16
+ disagreement** template instead.
17
+ - A security vulnerability in `aslmp` itself: see `SECURITY.md`, not here.
18
+
19
+ Everything below the first two boxes is asking for the conditions of your
20
+ observation. That is not bureaucracy: this project has twice had to withdraw a
21
+ published claim because a measurement was recorded without them, and a report
22
+ about a PLC with no firmware version behind it cannot be acted on.
23
+
24
+ - type: textarea
25
+ id: what-happened
26
+ attributes:
27
+ label: What happened, and what you expected instead
28
+ description: >
29
+ If an exception was raised, paste the whole diagnostic block — the target,
30
+ request, sent, received, routes, timing, observed, action and manual lines are
31
+ each load-bearing. Redact the address or the device values first if they matter
32
+ to you; say where you redacted.
33
+ placeholder: |
34
+ aslmp.SlmpUnsupportedCommandError: end code 0xC059 — "..."
35
+ target ...
36
+ request ...
37
+ validations:
38
+ required: true
39
+
40
+ - type: textarea
41
+ id: reproduction
42
+ attributes:
43
+ label: The smallest code that shows it
44
+ description: >
45
+ Against the simulator (`aslmp serve`, or `aslmp.testing`) if you can reproduce
46
+ it there — a simulator repro is one we can fix without your PLC. Say so if it
47
+ only reproduces against real silicon.
48
+ render: python
49
+ validations:
50
+ required: true
51
+
52
+ - type: input
53
+ id: aslmp-version
54
+ attributes:
55
+ label: aslmp version
56
+ description: "`aslmp --version`, or the commit SHA if you are on a checkout."
57
+ placeholder: 0.1.0.dev0
58
+ validations:
59
+ required: true
60
+
61
+ - type: input
62
+ id: python-and-os
63
+ attributes:
64
+ label: Python version and OS
65
+ description: >
66
+ `python -VV` and the OS. Both matter here more than usual: clock resolution and
67
+ socket behaviour differ across them, and one shipped bug in this library existed
68
+ only on Windows below CPython 3.13.
69
+ placeholder: "3.11.15, Windows 11"
70
+ validations:
71
+ required: true
72
+
73
+ - type: input
74
+ id: cpu-model
75
+ attributes:
76
+ label: CPU model
77
+ description: The exact model as printed on the unit, not the family.
78
+ placeholder: FX5U-32MT/DS
79
+ validations:
80
+ required: true
81
+
82
+ - type: input
83
+ id: firmware
84
+ attributes:
85
+ label: CPU firmware version
86
+ description: >
87
+ GX Works3 → Diagnostics → Module Diagnostics, the firmware version field. A
88
+ measurement names one piece of silicon on one firmware; a report without this is
89
+ not actionable, and "unknown" is a worse answer than a delayed one.
90
+ placeholder: "1.065"
91
+ validations:
92
+ required: true
93
+
94
+ - type: dropdown
95
+ id: profile
96
+ attributes:
97
+ label: Profile key
98
+ description: The string passed to `Plc(profile=...)`. `aslmp identify HOST` prints it.
99
+ options:
100
+ - "melsec:iq-f/fx5u"
101
+ - "melsec:iq-f/fx5uc"
102
+ - "melsec:iq-f/fx5uj"
103
+ - "melsec:iq-f/fx5s"
104
+ - "melsec:iq-r"
105
+ - "melsec:iq-r/r00"
106
+ - "melsec:q"
107
+ - "melsec:l"
108
+ - "no PLC involved (simulator, unit test, CLI)"
109
+ validations:
110
+ required: true
111
+
112
+ - type: dropdown
113
+ id: transport
114
+ attributes:
115
+ label: Transport
116
+ options:
117
+ - TCP
118
+ - UDP
119
+ - not applicable
120
+ validations:
121
+ required: true
122
+
123
+ - type: dropdown
124
+ id: frame
125
+ attributes:
126
+ label: Frame type
127
+ options:
128
+ - 3E
129
+ - 4E
130
+ - not applicable
131
+ validations:
132
+ required: true
133
+
134
+ - type: dropdown
135
+ id: data-code
136
+ attributes:
137
+ label: Communication Data Code
138
+ description: >
139
+ On iQ-F this is an **Own Node Setting for the whole Ethernet port**, not a
140
+ per-entry one. If you are not sure, that is itself worth saying — a coding
141
+ mismatch produces no response at all rather than an error.
142
+ options:
143
+ - Binary
144
+ - ASCII
145
+ - not sure
146
+ - not applicable
147
+ validations:
148
+ required: true
149
+
150
+ - type: textarea
151
+ id: connection-entry
152
+ attributes:
153
+ label: The connection entry, as GX Works3 has it
154
+ description: >
155
+ Port number, and anything unusual: a UDP entry's destination IP, how many entries
156
+ the CPU has configured, whether anything else was connected at the time. One TCP
157
+ connection is served per entry, so "something else was on it" explains a whole
158
+ class of report.
159
+ placeholder: |
160
+ TCP, PLC-side port 5002, one of six entries. Nothing else connected.
161
+ validations:
162
+ required: true
163
+
164
+ - type: input
165
+ id: host-and-link
166
+ attributes:
167
+ label: Host and link
168
+ description: >
169
+ Required if your report quotes a latency figure, and useful otherwise. Where the
170
+ client ran, what is between it and the CPU, and the median RTT if you know it.
171
+ This project published a transport conclusion that turned out to be a property of
172
+ its Wi-Fi link; every timing table in this repository now names these.
173
+ placeholder: "laptop at 192.168.10.41, Wi-Fi, ~7 ms median RTT"
174
+ validations:
175
+ required: false
176
+
177
+ - type: checkboxes
178
+ id: confirmations
179
+ attributes:
180
+ label: Before you post
181
+ options:
182
+ - label: >
183
+ This is not a security vulnerability report. Those follow `SECURITY.md` —
184
+ private reporting first, never a public issue.
185
+ required: true
186
+ - label: >
187
+ If I quoted a latency number, I named the host and the link it came from.
188
+ required: false
189
+ - label: >
190
+ I have read `docs/unverified.md` and this is not a path that already ships
191
+ labelled unverified.
192
+ required: false
@@ -0,0 +1,32 @@
1
+ # Blank issues are off because both templates exist to collect the conditions of an
2
+ # observation, and a report without them cannot be acted on. Anything that is not a bug
3
+ # and not a CPU disagreement is a question, and questions are better in Discussions,
4
+ # where the answer stays findable.
5
+ blank_issues_enabled: false
6
+
7
+ contact_links:
8
+ - name: Question, or "how do I ...?"
9
+ url: https://github.com/AcaysiaChem/aslmp/discussions
10
+ about: >
11
+ Setup, GX Works3 parameters, which profile to pass, why a read came back silent.
12
+ Ask here — a question is not a defect, and the README's PLC-side section grew out
13
+ of exactly these.
14
+
15
+ - name: Security vulnerability in aslmp
16
+ url: https://github.com/AcaysiaChem/aslmp/security/policy
17
+ about: >
18
+ Do not open a public issue. The reporting address and what we can and cannot fix
19
+ are in SECURITY.md.
20
+
21
+ - name: "SLMP has no authentication and no encryption"
22
+ url: https://github.com/AcaysiaChem/aslmp/blob/main/SECURITY.md
23
+ about: >
24
+ That is a property of the protocol, not a defect in this library, and it is not
25
+ something we can fix. Read SECURITY.md before reporting it.
26
+
27
+ - name: Is this path verified?
28
+ url: https://github.com/AcaysiaChem/aslmp/blob/main/docs/unverified.md
29
+ about: >
30
+ Seven of the eight shipped profiles have never been connected to, and the ASCII
31
+ path has never been on a wire. If what surprised you is on that page, an issue
32
+ saying so is still welcome — but the page may already answer it.
@@ -0,0 +1,159 @@
1
+ name: CPU behaviour disagreement
2
+ description: Your CPU answers differently from a shipped table, or you can close an unverified row.
3
+ title: "[cpu] "
4
+ labels: ["hardware", "provenance"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ **This is the report this project most wants.** One CPU is the whole of our
10
+ evidence: a MELSEC iQ-F FX5U-32MT/DS on firmware 1.065. Seven of the eight
11
+ shipped profiles have never been connected to, the ASCII path has never been on a
12
+ wire, and `docs/unverified.md` lists every one of them with the experiment that
13
+ would settle it. An iQ-R, a Q, an L, an FX5UC/FX5UJ/FX5S or an ASCII-coded port
14
+ turns a row of that page from "the manual says" into "we measured".
15
+
16
+ The data files are built to receive this: every row of every table in
17
+ `src/aslmp/data/` carries a provenance tail — cpu, firmware, measured, host,
18
+ medium, samples — and a row cannot be marked measured without them. The fields
19
+ below **are** those columns. Filling them in is most of the work of the change.
20
+
21
+ Safety first, as ever: do not probe a CPU that is attached to anything that can
22
+ move, and note that `aslmp verify-ranges` reads devices the profile would
23
+ normally refuse (it writes nothing and changes no CPU state).
24
+
25
+ - type: textarea
26
+ id: disagreement
27
+ attributes:
28
+ label: What we say, and what your CPU said
29
+ description: >
30
+ Name the table or the claim — a device range, a point limit, an end code, a model
31
+ code, a capability, a line in `docs/unverified.md` or `docs/hardware.md` — and put
32
+ the CPU's actual answer beside it. End codes and raw bytes are better than prose.
33
+ placeholder: |
34
+ ranges_iqr.tsv says melsec:iq-r R has 0 points. On this R04CPU, R32767 reads
35
+ 0x0000 and R32768 returns 0xC056, so the real boundary is 32768 points.
36
+ validations:
37
+ required: true
38
+
39
+ - type: input
40
+ id: cpu
41
+ attributes:
42
+ label: CPU model
43
+ description: The exact model, as printed on the unit. `Measurement.cpu`.
44
+ placeholder: R04CPU
45
+ validations:
46
+ required: true
47
+
48
+ - type: input
49
+ id: firmware
50
+ attributes:
51
+ label: Firmware version
52
+ description: >
53
+ `Measurement.firmware`. GX Works3 → Diagnostics → Module Diagnostics. A firmware
54
+ update can invalidate any row in this repository, which is why no row exists
55
+ without this.
56
+ placeholder: "1.065"
57
+ validations:
58
+ required: true
59
+
60
+ - type: input
61
+ id: date
62
+ attributes:
63
+ label: Date measured
64
+ description: "`Measurement.date`. ISO `YYYY-MM-DD`; the type refuses anything else."
65
+ placeholder: "2026-09-07"
66
+ validations:
67
+ required: true
68
+
69
+ - type: input
70
+ id: host
71
+ attributes:
72
+ label: Host
73
+ description: >
74
+ `Measurement.host` — where the client ran, precisely enough to go back to it: an
75
+ address, a name, or both.
76
+ placeholder: "192.168.10.36 (argus-bench)"
77
+ validations:
78
+ required: true
79
+
80
+ - type: input
81
+ id: medium
82
+ attributes:
83
+ label: Link
84
+ description: >
85
+ `Measurement.medium` — what ran between that host and the CPU, with the median RTT
86
+ if you know it. Leaving this out is the specific omission that cost this project a
87
+ published claim: a transport conclusion that was a property of one Wi-Fi link.
88
+ placeholder: "wired, 3.64 ms median RTT"
89
+ validations:
90
+ required: true
91
+
92
+ - type: input
93
+ id: samples
94
+ attributes:
95
+ label: n
96
+ description: >
97
+ `Measurement.samples` — how many observations the answer rests on. `1` is a fine
98
+ answer for "this end code came back"; a percentile without its n cannot be given an
99
+ error bar by a reader. Leave blank when a count is not what the claim rests on.
100
+ placeholder: "300"
101
+ validations:
102
+ required: false
103
+
104
+ - type: dropdown
105
+ id: what-it-touches
106
+ attributes:
107
+ label: What this would change
108
+ description: Best guess is fine; we will work it out in the thread.
109
+ multiple: true
110
+ options:
111
+ - "a device range (ranges_iqf.tsv / ranges_iqr.tsv)"
112
+ - "a point limit (limits.tsv)"
113
+ - "an end code (end_codes.tsv)"
114
+ - "a model code (model_codes.tsv)"
115
+ - "a profile capability"
116
+ - "an open ambiguity (ambiguities.tsv, an A-... key)"
117
+ - "a row of docs/unverified.md"
118
+ - "a figure in docs/hardware.md"
119
+ - "not sure"
120
+ validations:
121
+ required: true
122
+
123
+ - type: input
124
+ id: ambiguity-key
125
+ attributes:
126
+ label: Ambiguity key, if this closes one
127
+ description: >
128
+ `aslmp ambiguities` prints the table, each row with the experiment that would
129
+ settle it. If you ran one of those probes, name its key.
130
+ placeholder: "A-ZR-RADIX"
131
+ validations:
132
+ required: false
133
+
134
+ - type: textarea
135
+ id: evidence
136
+ attributes:
137
+ label: The evidence, as your tools printed it
138
+ description: >
139
+ Whatever you have: `aslmp identify`, `aslmp capabilities <key>`,
140
+ `aslmp verify-ranges HOST --profile KEY --python` (which emits a paste-ready
141
+ `with_ranges()` call), `aslmp.testing.run_conformance`, `aslmp proxy` frames, or
142
+ raw hex. Unedited output is worth more than a summary of it.
143
+ render: text
144
+ validations:
145
+ required: true
146
+
147
+ - type: checkboxes
148
+ id: confirmations
149
+ attributes:
150
+ label: Before you post
151
+ options:
152
+ - label: >
153
+ Nothing was written to a machine that can move something, and I did not issue
154
+ remote RUN/STOP/PAUSE/RESET/Latch Clear on a CPU I could not see.
155
+ required: true
156
+ - label: >
157
+ The CPU model and firmware above are the ones this observation came from, not
158
+ the family or a sibling unit.
159
+ required: true
@@ -0,0 +1,63 @@
1
+ <!--
2
+ Thank you. CONTRIBUTING.md has the long version of everything below; this is the short
3
+ list that gets a change merged. Delete the sections that do not apply, but do not delete
4
+ the provenance section if your diff touches a number.
5
+ -->
6
+
7
+ ## What this changes, and why
8
+
9
+ <!-- One paragraph. If it is a fix, say what the wrong behaviour was, not just what the
10
+ new behaviour is. -->
11
+
12
+ ## Checks
13
+
14
+ Run all three locally — CI runs them on 3.11, 3.12 and 3.13 across Linux, Windows and
15
+ macOS, and the matrix exists because a bug in this library was visible on exactly one of
16
+ those nine cells.
17
+
18
+ - [ ] `python -m ruff check src tests tools bench`
19
+ - [ ] `python -m mypy`
20
+ - [ ] `python -m pytest tests -q`
21
+
22
+ ## Does this touch a hardware claim?
23
+
24
+ A hardware claim is any number, limit, range, end code, capability or behaviour that is
25
+ asserted about real silicon — in a `.tsv`, in a docstring, in a doc, in a comment, or in
26
+ a test's expected value. **This project does not accept one without its conditions.**
27
+ Two published claims have been withdrawn here, and both were prose figures whose
28
+ conditions were never written down.
29
+
30
+ If your diff has one, fill this in; if it does not, write "no hardware claim" and move on.
31
+
32
+ - **CPU model:**
33
+ - **Firmware version:**
34
+ - **Date measured (ISO):**
35
+ - **Host:** <!-- where the client ran; an address, a name, or both -->
36
+ - **Link:** <!-- wired / Wi-Fi, with median RTT if known -->
37
+ - **n:** <!-- samples behind the figure, or "not a sample-based claim" -->
38
+
39
+ - [ ] The claim is expressed as a `Measurement(...)` / `Evidence.measured(...)`, not as a
40
+ bare string, so the provenance and the source cannot drift apart.
41
+ - [ ] Provenance is `LIVE` only where silicon was actually observed. A manual reading is
42
+ `MANUAL`; a value carried across from a sibling model is `INFERRED`, and says so.
43
+ - [ ] If this converts an unverified row to a measured one, `docs/unverified.md` was
44
+ updated in the same commit. That page is billed as complete and a test enforces it.
45
+
46
+ ## Everything else
47
+
48
+ - [ ] No new runtime dependency. The package has zero, deliberately: the wheel has to
49
+ install on a Jetson's aarch64 and on a plant PC with no compiler.
50
+ - [ ] Nothing retries, clamps, substitutes a default, or returns a stale value. An
51
+ AST-level test enforces this over the whole package; if you had to work around it,
52
+ say so here rather than widening the exemption.
53
+ - [ ] No new path from the CLI to remote RUN / STOP / PAUSE / RESET / Latch Clear. A
54
+ shell history is not an interlock, and a test walks the AST of `aslmp/tools` to
55
+ prove there is no such path.
56
+ - [ ] Public surface changes are reflected in `aslmp.__all__` and `CHANGELOG.md`.
57
+ - [ ] If this makes an unverified path look verified, or reads as a promise about safety,
58
+ certification or conformance, it does not go in. This library is not a safety
59
+ system, holds no conformance certification, and has no affiliation with Mitsubishi
60
+ Electric or the CC-Link Partner Association.
61
+
62
+ By opening this pull request you are offering the contribution under the project's
63
+ Apache-2.0 licence (see section 5 of `LICENSE`).
@@ -0,0 +1,25 @@
1
+ # GitHub Actions only, and that is not an oversight.
2
+ #
3
+ # `aslmp` has zero runtime dependencies -- deliberately and load-bearing: the wheel has
4
+ # to install on a Jetson's aarch64 and on a locked-down plant PC with no compiler and no
5
+ # proxy to PyPI's transitive graph. So there is no `pip` ecosystem entry here, because
6
+ # there is nothing in it to watch. The `dev` extra (pytest, hypothesis, mypy, ruff) is
7
+ # declared as unpinned floors rather than pins, so a bump would have nothing to change;
8
+ # CI installs whatever is current on each run, which is the honest way to find out that a
9
+ # new mypy disagrees with us.
10
+ #
11
+ # The actions in `.github/workflows/` are pinned by major version, which is the one place
12
+ # this repository does carry a supply chain -- a workflow action runs with a checkout of
13
+ # our source and, in publish.yml, beside an OIDC token.
14
+
15
+ version: 2
16
+
17
+ updates:
18
+ - package-ecosystem: github-actions
19
+ directory: "/"
20
+ schedule:
21
+ interval: weekly
22
+ commit-message:
23
+ prefix: "ci"
24
+ labels:
25
+ - dependencies
@@ -0,0 +1,111 @@
1
+ # The three checks this project ships under, on every interpreter and every OS it claims.
2
+ #
3
+ # Why the matrix is nine jobs and not one
4
+ # ---------------------------------------
5
+ # This project shipped a real bug that exactly one cell of this matrix would have caught.
6
+ # `time.monotonic()` is backed by `GetTickCount64()` on Windows before CPython 3.13 and
7
+ # steps at 15.625 ms, so on the oldest interpreter this package claims, on Windows, every
8
+ # stamp it took landed on a ~16 ms grid: a 6 ms round trip was recorded as 0.0 or 16.0.
9
+ # Measured 2026-09-12 on Python 3.11.15, Windows 11, against `perf_counter_ns` on the same
10
+ # machine and the same sleep. It was invisible during development, which happens on 3.13,
11
+ # where `monotonic` is `QueryPerformanceCounter` and resolves to 100 ns; it surfaced only
12
+ # when the library was driven from a 3.11 environment. The fix is `src/aslmp/_clock.py`.
13
+ #
14
+ # "Latency as data" is this library's whole argument, so a quantised latency figure is
15
+ # worse than an absent one. A single-version, single-OS CI would not have found that and
16
+ # will not find the next one. The matrix is here for that reason and not for tidiness.
17
+ #
18
+ # Note that `mypy` pins `python_version = "3.11"` in `pyproject.toml`, so the interpreter
19
+ # axis does not change what it type-checks -- the OS axis does, wherever behaviour is
20
+ # platform-conditional, and that is the half of this matrix that earns its keep for mypy.
21
+
22
+ name: CI
23
+
24
+ on:
25
+ push:
26
+ branches: [main]
27
+ pull_request:
28
+ workflow_dispatch:
29
+
30
+ permissions:
31
+ contents: read
32
+
33
+ concurrency:
34
+ group: ci-${{ github.workflow }}-${{ github.ref }}
35
+ cancel-in-progress: true
36
+
37
+ jobs:
38
+ checks:
39
+ name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
40
+ runs-on: ${{ matrix.os }}
41
+
42
+ # A healthy cell finishes in about two minutes. This bound is here because four
43
+ # cells of this matrix once sat INSIDE the pytest step for fourteen minutes and were
44
+ # still going when an unrelated push happened to cancel the run (2026-09-24, commit
45
+ # d78ef84: macos/3.12, macos/3.13, ubuntu/3.12 and windows/3.12 hung while
46
+ # windows/3.13 passed the same step in 109 s). Nothing stopped them but that
47
+ # accident. GitHub's default job timeout is 360 minutes, so each of those cells was
48
+ # entitled to six hours, and a macOS minute bills at ten times a Linux one.
49
+ #
50
+ # Fifteen minutes is roughly seven times the honest run and still bounds a hung
51
+ # macOS cell to about a dollar instead of twenty-two.
52
+ timeout-minutes: 15
53
+ strategy:
54
+ # Never fail fast. The point of the matrix is to see WHICH cells disagree; a
55
+ # cancelled run hides exactly the pattern that identifies a clock or a path bug.
56
+ fail-fast: false
57
+ matrix:
58
+ os: [ubuntu-latest, windows-latest, macos-latest]
59
+ python-version: ["3.11", "3.12", "3.13"]
60
+
61
+ defaults:
62
+ run:
63
+ # bash on all three runners, so the quoting in these steps is written once.
64
+ shell: bash
65
+
66
+ steps:
67
+ - uses: actions/checkout@v7
68
+
69
+ - uses: actions/setup-python@v7
70
+ with:
71
+ python-version: ${{ matrix.python-version }}
72
+ # There is nothing to lock -- this package has zero runtime dependencies -- but
73
+ # the dev extra pulls a toolchain on every one of nine cells, and two of those
74
+ # runners bill by the minute.
75
+ cache: pip
76
+ cache-dependency-path: pyproject.toml
77
+
78
+ - name: Install
79
+ run: |
80
+ python -m pip install --upgrade pip
81
+ python -m pip install -e ".[dev]"
82
+
83
+ # Hardware tests are excluded on purpose, and this step says so rather than
84
+ # leaving it to the marker. tests/hardware/ is gated on ASLMP_TEST_HOST and drives
85
+ # a real FX5U: it writes to scratch registers, and tests/hardware/
86
+ # test_remote_control.py issues Remote STOP, which halts a CPU. A CI runner must
87
+ # never be able to reach a PLC, so the variable is not set here and must not be
88
+ # added as a repository or environment secret.
89
+ - name: Assert the hardware gate is not set
90
+ run: |
91
+ if [ -n "${ASLMP_TEST_HOST:-}" ]; then
92
+ echo "ASLMP_TEST_HOST is set in CI. Hardware tests write to a real PLC and"
93
+ echo "can halt a CPU. Remove it from the repository/environment settings."
94
+ exit 1
95
+ fi
96
+
97
+ - name: ruff
98
+ run: python -m ruff check src tests tools bench
99
+
100
+ - name: mypy --strict
101
+ run: python -m mypy
102
+
103
+ # `-m "not hardware"` is belt and braces beside the ASLMP_TEST_HOST gate: the
104
+ # marker is the declared contract, the missing variable is what actually makes the
105
+ # tests skip, and neither one alone is worth relying on for something that can stop
106
+ # a machine.
107
+ # `faulthandler_timeout` in pyproject.toml is what makes a hang here legible: the
108
+ # stacks land in this step's log about a minute in, long before `timeout-minutes`
109
+ # kills the job, so the log names the test and the await instead of just stopping.
110
+ - name: pytest
111
+ run: python -m pytest tests -q -m "not hardware" --durations=10
@@ -0,0 +1,94 @@
1
+ # Build a release and publish it to PyPI with trusted publishing (OIDC).
2
+ #
3
+ # There is no API token anywhere in this file and there must never be one. Trusted
4
+ # publishing mints a short-lived, workflow-scoped credential at run time; a long-lived
5
+ # token in a repository secret is a credential that can publish `aslmp` from anywhere,
6
+ # forever, and this package installs on machines that talk to PLCs.
7
+ #
8
+ # WHAT THE OWNER STILL HAS TO DO -- none of it can be done from this repository
9
+ # ----------------------------------------------------------------------------
10
+ # 1. On PyPI, add a *pending* trusted publisher before the first release
11
+ # (https://pypi.org/manage/account/publishing/), because the project name is not
12
+ # registered yet -- README.md says so in its Install section. The four fields are:
13
+ # PyPI project name: aslmp
14
+ # Owner: AcaysiaChem
15
+ # Repository name: aslmp
16
+ # Workflow name: publish.yml
17
+ # Environment name: pypi
18
+ # All five must match exactly, including the environment, or the publish step is
19
+ # rejected with an OIDC error rather than a permissions one.
20
+ # 2. Create a GitHub environment named `pypi` (Settings -> Environments) and, if you
21
+ # want a human in the loop, add yourself as a required reviewer there. That is the
22
+ # only gate between "someone pushed a release" and "a new aslmp is on PyPI".
23
+ # 3. Publish a GitHub release whose tag is the version, e.g. `v0.1.0`. This workflow
24
+ # refuses to publish if that tag disagrees with `src/aslmp/_version.py`.
25
+ #
26
+ # Two jobs on purpose: `build` needs no privileges at all, and `publish` holds the OIDC
27
+ # token and runs nothing but the upload. Nothing that touches the internet or the
28
+ # project's own code executes in the job that can sign.
29
+
30
+ name: Publish
31
+
32
+ on:
33
+ release:
34
+ types: [published]
35
+
36
+ permissions:
37
+ contents: read
38
+
39
+ jobs:
40
+ build:
41
+ name: Build sdist and wheel
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v7
45
+
46
+ - uses: actions/setup-python@v7
47
+ with:
48
+ python-version: "3.12"
49
+
50
+ - name: Check the tag against src/aslmp/_version.py
51
+ run: |
52
+ set -euo pipefail
53
+ version=$(python -c "import runpy; print(runpy.run_path('src/aslmp/_version.py')['__version__'])")
54
+ tag="${GITHUB_REF_NAME#v}"
55
+ echo "tag=$tag _version.py=$version"
56
+ if [ "$tag" != "$version" ]; then
57
+ echo "Release tag $GITHUB_REF_NAME does not match __version__ $version."
58
+ echo "The version literal exists once, in src/aslmp/_version.py. Fix one of them."
59
+ exit 1
60
+ fi
61
+
62
+ - name: Build
63
+ run: |
64
+ python -m pip install --upgrade pip build twine
65
+ python -m build
66
+ python -m twine check --strict dist/*
67
+
68
+ - uses: actions/upload-artifact@v7
69
+ with:
70
+ name: dist
71
+ path: dist/
72
+ if-no-files-found: error
73
+
74
+ publish:
75
+ name: Publish to PyPI
76
+ needs: build
77
+ runs-on: ubuntu-latest
78
+ # The environment is half of the trusted-publisher match configured on PyPI, and it
79
+ # is where a required reviewer goes if you want one.
80
+ environment:
81
+ name: pypi
82
+ url: https://pypi.org/p/aslmp
83
+ permissions:
84
+ # The OIDC token. This is the only privilege this job has, and the only job that
85
+ # has it.
86
+ id-token: write
87
+ steps:
88
+ - uses: actions/download-artifact@v8
89
+ with:
90
+ name: dist
91
+ path: dist/
92
+
93
+ # `release/v1` is PyPA's own major-version ref for this action.
94
+ - uses: pypa/gh-action-pypi-publish@release/v1