Skip to content

Commit ba449d3

Browse files
committed
Systematize Memory Configuration bitfield constants (Make them fully cross-checkable and add related tests).
1 parent e7aca7f commit ba449d3

2 files changed

Lines changed: 114 additions & 21 deletions

File tree

openlcb/memoryservice.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ class MCOp(Enum):
5353
"""Byte 1 & 0b11111100 values (assumes byte 0 is 0x20)"""
5454
Read_Command = 0x40 # 01000000
5555
Read_Reply = 0x50 # 01010000
56-
Read_Reply_Failure = 0x58 # 01011000
56+
Read_Reply_Failure = 0x58 # 01011000; See OP_FAILURE_BYTES
5757
Read_Stream_Command = 0x60 # 01100000
5858
Read_Stream_Reply = 0x70 # 01110000
59-
Read_Stream_Reply_Failure = 0x78 # 01111000
59+
Read_Stream_Reply_Failure = 0x78 # 01111000; See OP_FAILURE_BYTES
6060
Write_Command = 0x00
6161
Write_Reply = 0x10 # 00010000
6262
# or 00010001 (0x11) and so on
63-
Write_Reply_Failure = 0x18
63+
Write_Reply_Failure = 0x18 # 00011000; See OP_FAILURE_BYTES
6464
Write_Under_Mask_Command = 0x08 # 00001000
6565
Write_Stream_Command = 0x20 # 01000000
6666
Write_Stream_Reply = 0x30 # 00110000
@@ -109,30 +109,42 @@ class MCOpMasks:
109109
# they can be broken down with
110110
# sub-checks even if Default is used
111111
# (See any set with more than one entry
112-
# in MODE_BYTES):
112+
# in TWO_BIT_PARAMS):
113113
# Get_Configuration_Options = 0x11111110
114114
# Get_Address_Space_Info = 0x11111110
115115

116116

117-
MODE_BYTES = {
118-
# order determines meaning for lists (See )
119-
MCOp.Read_Command.value: {0x40, 0x41, 0x42, 0x43}, # pool
120-
MCOp.Read_Reply.value: {0x50, 0x51, 0x52, 0x53},
121-
MCOp.Read_Stream_Command.value: {0x60, 0x61, 0x62, 0x63}, # pool
122-
MCOp.Read_Stream_Reply.value: {0x70, 0x71, 0x72, 0x73}, # TODO
117+
TWO_BIT_PARAMS = {
118+
# The combined values in these lists include all 6-bit
119+
# operation groups *except* those in OP_FAILURE_BYTES
120+
# order determines meaning for lists:
121+
# - first indicates custom space follows (in later byte)
122+
# - Others are standard spaces
123+
# See MemorySpaceIndex enum for meaning of last 2 bits
124+
# (and Configuration Description Information Standard).
125+
# - does *not* apply to *sets* below!
126+
# *lists* (last 2 bits are MemorySpaceIndex.fromNumber param)
127+
MCOp.Read_Command.value: [0x40, 0x41, 0x42, 0x43], # pool
128+
MCOp.Read_Reply.value: [0x50, 0x51, 0x52, 0x53],
129+
MCOp.Read_Stream_Command.value: [0x60, 0x61, 0x62, 0x63], # pool
130+
MCOp.Read_Stream_Reply.value: [0x70, 0x71, 0x72, 0x73], # TODO
123131
MCOp.Write_Command.value: [0x00, 0x01, 0x02, 0x03], # pool
124-
MCOp.Write_Reply.value: {0x10, 0x11, 0x12, 0x13},
125-
MCOp.Write_Under_Mask_Command.value: {0x08, 0x09, 0x0A, 0x0B}, # pool
126-
MCOp.Write_Stream_Command.value: {0x20, 0x21, 0x22, 0x23},
127-
MCOp.Write_Stream_Reply.value: {0x30, 0x31, 0x32, 0x33}, # TODO
132+
MCOp.Write_Reply.value: [0x10, 0x11, 0x12, 0x13],
133+
MCOp.Write_Under_Mask_Command.value: [0x08, 0x09, 0x0A, 0x0B], # pool
134+
MCOp.Write_Stream_Command.value: [0x20, 0x21, 0x22, 0x23],
135+
MCOp.Write_Stream_Reply.value: [0x30, 0x31, 0x32, 0x33], # TODO
136+
# *sets* (no standard meaning to last 2 bits)
128137
MCOp.Get_Configuration_Options_Command.value: {0x80, },
129138
MCOp.Get_Configuration_Options_Reply.value: {0x82, },
130139
MCOp.Get_Address_Space_Info_Command.value: {
131140
MCOp.Get_Address_Space_Info_Command.value,
132141
MCOp.Get_Address_Space_Info_Reply.value,
133142
MCOp.Get_Address_Space_Info_Reply_Command.value,
134143
},
135-
MCOp.Lock_or_Reserve_Command.value: {0x88, },
144+
MCOp.Lock_or_Reserve_Command.value: {
145+
MCOp.Lock_or_Reserve_Command.value,
146+
MCOp.Lock_or_Reserve_Reply.value,
147+
},
136148
MCOp.Get_Unique_ID_Command.value: {0x8C, },
137149
MCOp.Get_Unique_ID_Reply.value: {0x8D, },
138150
MCOp.Unfreeze_Command.value: {0xA1, 0xA0}, # unfreeze, freeze respectively
@@ -143,11 +155,21 @@ class MCOpMasks:
143155
}
144156
}
145157

146-
MODE_ERROR_BYTES = {
147-
MCOp.Read_Reply.value: {0x58, 0x59, 0x5A, 0x5B},
148-
MCOp.Read_Stream_Reply.value: {0x78, 0x79, 0x7A, 0x7B},
149-
MCOp.Write_Reply.value: {0x18, 0x19, 0x1A, 0x1B},
150-
MCOp.Write_Stream_Reply.value: {0x38, 0x39, 0x3A, 0x3B},
158+
OP_FAILURE_BYTES = {
159+
# *lists* (last 2 bits are standard memory space):
160+
MCOp.Read_Reply.value: [0x58, 0x59, 0x5A, 0x5B],
161+
MCOp.Read_Stream_Reply.value: [0x78, 0x79, 0x7A, 0x7B],
162+
MCOp.Write_Reply.value: [0x18, 0x19, 0x1A, 0x1B],
163+
MCOp.Write_Stream_Reply.value: [0x38, 0x39, 0x3A, 0x3B],
164+
# *sets* (last 2 bits have no special meaning):
165+
MCOp.Read_Reply_Failure.value: {
166+
MCOp.Read_Reply_Failure.value},
167+
MCOp.Write_Reply_Failure.value: {
168+
MCOp.Write_Reply_Failure.value},
169+
MCOp.Read_Stream_Reply_Failure.value: {
170+
MCOp.Read_Stream_Reply_Failure.value},
171+
MCOp.Write_Stream_Reply_Failure.value: {
172+
MCOp.Write_Stream_Reply_Failure.value},
151173
}
152174

153175

@@ -280,7 +302,7 @@ def parseReplyDatagram(memo: Union[MemoryReadMemo, MemoryWriteMemo],
280302
pass
281303
# 0x08 (0b00001000) is error bit
282304
# mode = None
283-
# for k, values in MODE_ERROR_BYTES.items():
305+
# for k, values in OP_FAILURE_BYTES.items():
284306
# if dmemo.data[1] in values:
285307
# mode = k
286308
# break
@@ -430,6 +452,9 @@ def datagramReceivedListener(self, dmemo: DatagramReadMemo) -> bool:
430452
if dmemo.data[1] in (0x50, 0x51, 0x52, 0x53, 0x58, 0x59, 0x5A, 0x5B):
431453
assert mcOp is MCOp.Read_Reply, \
432454
"self-test failed (bad constant(s))"
455+
assert (dmemo.data[1] in TWO_BIT_PARAMS[mcOp.value]
456+
or dmemo.data[1] in OP_FAILURE_BYTES[mcOp.value]), \
457+
"self-test failed (bad constant(s))"
433458
# MCOp.Read_Reply
434459
# read or read-error reply
435460

tests/test_memoryservice.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: utf-8 -*-
2+
from collections import OrderedDict
23
import os
34
import struct
45
import sys
@@ -30,6 +31,9 @@
3031
from openlcb.mti import MTI # noqa: E402
3132
from openlcb.message import Message # noqa: E402
3233
from openlcb.memoryservice import ( # noqa: E402
34+
OP_FAILURE_BYTES,
35+
TWO_BIT_PARAMS,
36+
MCOp,
3337
MemoryReadMemo,
3438
MemoryWriteMemo,
3539
MemoryService,
@@ -183,6 +187,70 @@ def testMultipleRead(self):
183187
self.assertEqual(len(LinkMockLayer.sentMessages), 5) # read reply datagram reply sent and next datagram sent # noqa: E501
184188
self.assertEqual(len(self.returnedMemoryReadMemo), 2) # memory read returned # noqa: E501
185189

190+
def testProtocolGroupUniqueness(self):
191+
"""Ensure each 6-bit field is unique"""
192+
opCounts = OrderedDict()
193+
194+
def incrementKey(key, counts):
195+
if key not in counts:
196+
counts[key] = 1
197+
return
198+
raise AssertionError(
199+
f"Bitfield {hex(key)} applies to more than one parent op")
200+
# counts[key] += 1
201+
202+
for op in MCOp:
203+
incrementKey(op.value, opCounts)
204+
# Ensure that 6-bit fields are systematized (correct constants)
205+
if ((op.value in OP_FAILURE_BYTES)
206+
and (len(OP_FAILURE_BYTES[op.value]) == 1)):
207+
assert op.value not in TWO_BIT_PARAMS, \
208+
(f"{op} last 2 bits are not significant"
209+
" so it should not be in TWO_BIT_PARAMS dict")
210+
# elif op.value & 0b11111100 in OP_FAILURE_BYTES:
211+
# for _, failureBytes in \
212+
# OP_FAILURE_BYTES[op.value & 0b11111100].items():
213+
# for failureByte in failureBytes:
214+
# assert failureByte not in TWO_BIT_PARAMS[op.value], \
215+
# (f"Failure bytes should not be in two-bit"
216+
# f" params for {op}")
217+
# assert isinstance(OP_FAILURE_BYTES[op.value], list), \
218+
# (f"If {op} use last 2 bits as index,"
219+
# " it should be recorded as a list in OP_FAILURE_BYTES")
220+
# elif op.value in OP_FAILURE_BYTES:
221+
# assert isinstance(OP_FAILURE_BYTES[op.value], set), \
222+
# (f"If {op} doesn't use last 2 bits as index,"
223+
# " it should be recorded as a set")
224+
elif op.value in TWO_BIT_PARAMS:
225+
if op.value in OP_FAILURE_BYTES:
226+
assert isinstance(OP_FAILURE_BYTES[op.value], list), \
227+
(f"If {op} use last 2 bits as index, it"
228+
" should be recorded as a list in OP_FAILURE_BYTES")
229+
# elif "Reply" in str(op):
230+
# # commented since not a real problem
231+
# # MCOp.Get_Configuration_Options_Reply doesn't have a
232+
# # corresponding error for the same 6-bit op field.
233+
# raise AssertionError(
234+
# (f"{op} is a reply but does not have an error reply."
235+
# " Does this follow the standard? If so,"
236+
# "remove this assertion."))
237+
else:
238+
# assert op.value in TWO_BIT_PARAMS, \
239+
# f"There is no list of two-bit params for {op}"
240+
# ^ Incorrect assertion since some share 1st 6 bits and
241+
# don't have params. See below instead.
242+
parentValue = op.value & 0b11111100
243+
assert (op.value in TWO_BIT_PARAMS
244+
or ((parentValue in TWO_BIT_PARAMS)
245+
and (op.value in TWO_BIT_PARAMS[parentValue]))
246+
), \
247+
f"There is no list of two-bit params for {op}"
248+
249+
for opValue, _ in TWO_BIT_PARAMS.items():
250+
parentValue = opValue & 0b11111100
251+
assert opCounts.get(parentValue) == 1, \
252+
f"op {hex(opValue)} is not in MCOp parents enum"
253+
186254

187255
if __name__ == '__main__':
188256
unittest.main()

0 commit comments

Comments
 (0)