🚧 Change module numbering

This commit is contained in:
Jonas Bengtsson
2026-10-04 13:05:47 +02:00
parent 7948b794f5
commit e5df0b04ed
17 changed files with 644 additions and 138 deletions
+39 -27
View File
@@ -88,13 +88,15 @@ class TestAliases:
class TestModuleAddressing:
"""Module type + module number + channel number addressing (DI0.1).
The pump node fixture modules: [DI_8, DI_16, DO_8, DO_16, AI_2, AO_2]
(indices 0..5). Module addressing uses 0-based module positions and
0-based channel numbers on the module.
The pump node fixture modules: [DI_8, DI_16, DO_8, DO_16, AI_2, AO_2].
Module addressing (FR-4.3.11 v1.5) numbers modules 0-based WITHIN
THEIR TYPE: DI0=DI_8, DI1=DI_16, DO0=DO_8, DO1=DO_16, AI0=AI_2,
AO0=AO_2 — regardless of physical slot interleaving. Channels are
0-based on the module.
"""
def test_module_addressing_di(self, node, datastore):
# DI0.1: module slot 0 (DI_8), channel 1 (0-based) = 2nd DI
# DI0.1: DI module 0 (DI_8), channel 1 (0-based) = 2nd DI
scenario = _scenario()
scenario["inputs"]["second_di"] = {"type": "di", "module": 0, "channel": 1}
engine = ScenarioEngine(node, scenario)
@@ -104,35 +106,42 @@ class TestModuleAddressing:
assert engine.alias_value("second_di") is True
def test_module_addressing_across_modules(self, node, datastore):
# DI1.0: module slot 1 (DI_16), channel 0 = 9th DI globally
# (8 channels on module 0 come first)
# DI1.0: DI module 1 (DI_16, physical slot 1), channel 0 = 9th DI
scenario = _scenario()
scenario["inputs"]["di16_first"] = {"type": "di", "module": 1, "channel": 0}
engine = ScenarioEngine(node, scenario)
engine.set_alias("di16_first", True)
assert datastore.read_bits(8, 1) == [True]
def test_module_addressing_do(self, node, datastore):
# DO2.2: module slot 2 (DO_8), channel 2 = 3rd DO globally
def test_module_addressing_do_interleaved(self, node, datastore):
# DO0.2: DO module 0 is the DO_8 at PHYSICAL SLOT 2 — per-type
# numbering means module 0, not slot 2
scenario = _scenario()
scenario["outputs"]["third_do"] = {"type": "do", "module": 2, "channel": 2}
scenario["outputs"]["third_do"] = {"type": "do", "module": 0, "channel": 2}
engine = ScenarioEngine(node, scenario)
bit = engine.alias_location("third_do").bit
assert bit == 2
assert datastore.write_bits(bit, [True]) is None
assert engine.alias_value("third_do") is True
def test_module_addressing_ai_ao(self, node, datastore):
# AI4.1: module slot 4 (AI_2), channel 1 = 2nd AI word
def test_do_module_1_is_do16(self, node):
# DO1.0: DO module 1 is the DO_16 at physical slot 3
scenario = _scenario()
scenario["inputs"]["ai2"] = {"type": "ai", "module": 4, "channel": 1, "scale": 1.0}
scenario["outputs"]["x"] = {"type": "do", "module": 1, "channel": 0}
engine = ScenarioEngine(node, scenario)
assert engine.alias_location("x").bit == 8 # after DO_8's 8 channels
def test_module_addressing_ai_ao(self, node, datastore):
# AI0.1: AI module 0 (AI_2 at physical slot 4), channel 1 = 2nd AI word
scenario = _scenario()
scenario["inputs"]["ai2"] = {"type": "ai", "module": 0, "channel": 1, "scale": 1.0}
engine = ScenarioEngine(node, scenario)
engine.set_alias("ai2", 0x1234)
loc = node.channel("ai", 2) # global 2nd AI
assert datastore.get_input_word(loc.word) == 0x1234
# AO5.1: module slot 5 (AO_2), channel 1 = 2nd AO word
scenario["outputs"] = {"ao2": {"type": "ao", "module": 5, "channel": 1}}
# AO0.1: AO module 0 (AO_2 at physical slot 5), channel 1 = 2nd AO word
scenario["outputs"] = {"ao2": {"type": "ao", "module": 0, "channel": 1}}
engine2 = ScenarioEngine(node, scenario)
bit_word = node.channel("ao", 2).word
assert datastore.write_words(bit_word, [0xBEEF]) is None
@@ -149,31 +158,34 @@ class TestModuleAddressing:
def test_module_out_of_range_rejected(self, node):
scenario = _scenario()
scenario["inputs"]["bad"] = {"type": "di", "module": 99, "channel": 0}
with pytest.raises(IndexError, match="module 99"):
# the node has only 2 DI modules
scenario["inputs"]["bad"] = {"type": "di", "module": 2, "channel": 0}
with pytest.raises(IndexError, match="DI module 2"):
ScenarioEngine(node, scenario)
def test_channel_out_of_range_rejected(self, node):
# module 0 is a DI_8 -> channels 0-7 only
# DI module 0 is a DI_8 -> channels 0-7 only
scenario = _scenario()
scenario["inputs"]["bad"] = {"type": "di", "module": 0, "channel": 8}
with pytest.raises(IndexError, match="channel 8"):
ScenarioEngine(node, scenario)
def test_kind_mismatch_rejected(self, node):
# module 0 is DI-only -> DO addressing must fail
def test_module_kind_routes_to_own_type(self, node):
"""Per-type numbering (v1.5): 'do' module numbers address the DO
modules regardless of physical slot — DO0 is the DO_8 at slot 2.
A 'do' alias on module 1 (the DO_16) is valid and distinct."""
scenario = _scenario()
scenario["outputs"]["bad"] = {"type": "do", "module": 0, "channel": 0}
from wago_sim.modules import ChannelMismatch
with pytest.raises(ChannelMismatch):
ScenarioEngine(node, scenario)
scenario["outputs"]["do8"] = {"type": "do", "module": 0, "channel": 0}
scenario["outputs"]["do16"] = {"type": "do", "module": 1, "channel": 0}
engine = ScenarioEngine(node, scenario)
assert engine.alias_location("do8").bit == 0
assert engine.alias_location("do16").bit == 8
def test_analog_kind_on_digital_module_rejected(self, node):
# module 0 is DI-only -> AI addressing must fail
# per-type routing: 'ai' module 9 is out of range (only 1 AI module)
scenario = _scenario()
scenario["inputs"]["bad"] = {"type": "ai", "module": 0, "channel": 0}
from wago_sim.modules import ChannelMismatch
with pytest.raises(ChannelMismatch):
scenario["inputs"]["bad"] = {"type": "ai", "module": 9, "channel": 0}
with pytest.raises(IndexError):
ScenarioEngine(node, scenario)