Skip to content

Commit 2548035

Browse files
committed
Make SCP errors more descriptive.
* All SCPError derrived exceptions now include a summary of the offending packet (most importantly x, y, p and command!). The SCPPacket object is also made accessible from the exc.packet attribute. * RC errors also now contain a human readable explanation of what the error codes mean. * Instead of having many RC error exception types, there is now one FatalReturnCodeError exception type. If differentiating between the types is important, the RC is included in the exc.return_code attribute. If required in the future, subclasses can (of course) be created for individual return codes without breaking backward compatibility. This change (strictly speaking) breaks backward compatibility as it renames exceptions of the RC-specific types. @mundya are you using these exceptions anywhere and is breaking compatibility here a problem?
1 parent 0c4ad65 commit 2548035

File tree

5 files changed

+139
-64
lines changed

5 files changed

+139
-64
lines changed

rig/machine_control/consts.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,6 @@
1818
produce (256 + 8 bytes).
1919
"""
2020

21-
SCP_RC_OK = 0x80
22-
"""SCP response code which indicates that everything was fine."""
23-
24-
SCP_RC_TIMEOUT = {0x8b, 0x8c, 0x8d, 0x8e}
25-
"""SCP response codes which should be treated as if they were a packet timing
26-
out.
27-
"""
28-
2921
SPINNAKER_RTR_BASE = 0xE1000000 # Unbuffered
3022
"""Base address of router hardware registers."""
3123

@@ -93,6 +85,58 @@ class SCPCommands(enum.IntEnum):
9385
power = 57 # BMP main board power control
9486

9587

88+
@add_int_enums_to_docstring
89+
class SCPReturnCodes(enum.IntEnum):
90+
"""SCP return codes"""
91+
ok = 0x80 # Command completed OK
92+
len = 0x81 # Bad packet length (Fatal)
93+
sum = 0x82 # Bad checksum (Retryable)
94+
cmd = 0x83 # Bad/invalid command (Fatal)
95+
arg = 0x84 # Invalid arguments (Fatal)
96+
port = 0x85 # Bad port number (Fatal)
97+
timeout = 0x86 # Monitor <-> app-core comms timeout (Fatal)
98+
route = 0x87 # No P2P route (Fatal)
99+
cpu = 0x88 # Bad CPU number (Fatal)
100+
dead = 0x89 # SHM dest dead (Fatal)
101+
buf = 0x8a # No free SHM buffers (Fatal)
102+
p2p_noreply = 0x8b # No reply to open (Fatal)
103+
p2p_reject = 0x8c # Open rejected (Fatal)
104+
p2p_busy = 0x8d # Dest busy (Retryable)
105+
p2p_timeout = 0x8e # Eth chip <--> destination comms timeout (Fatal)
106+
pkt_tx = 0x8f # Pkt Tx failed (Fatal)
107+
108+
RETRYABLE_SCP_RETURN_CODES = set([
109+
SCPReturnCodes.sum,
110+
SCPReturnCodes.p2p_busy,
111+
])
112+
"""The set of :py:class:`.SCPReturnCodes` values which indicate a non-fatal
113+
retryable fault."""
114+
115+
116+
FATAL_SCP_RETURN_CODES = {
117+
SCPReturnCodes.len: "Bad command length.",
118+
SCPReturnCodes.cmd: "Bad/invalid command.",
119+
SCPReturnCodes.arg: "Invalid command arguments.",
120+
SCPReturnCodes.port: "Bad port number.",
121+
SCPReturnCodes.timeout:
122+
"Timeout waiting for the application core to respond to "
123+
"the monitor core's request.",
124+
SCPReturnCodes.route: "No P2P route to the target chip is available.",
125+
SCPReturnCodes.cpu: "Bad CPU number.",
126+
SCPReturnCodes.dead: "SHM dest dead.",
127+
SCPReturnCodes.buf: "No free SHM buffers.",
128+
SCPReturnCodes.p2p_noreply:
129+
"No response packets from the target reached the "
130+
"ethernet connected chip.",
131+
SCPReturnCodes.p2p_reject: "The target chip rejected the packet.",
132+
SCPReturnCodes.p2p_timeout:
133+
"Communications between the ethernet connected chip and target chip "
134+
"timedout.",
135+
SCPReturnCodes.pkt_tx: "Packet transmission failed.",
136+
}
137+
"""The set of fatal SCP errors and a human-readable error."""
138+
139+
96140
@add_int_enums_to_docstring
97141
class DataType(enum.IntEnum):
98142
"""Used to specify the size of data being read to/from a SpiNNaker machine

rig/machine_control/packets.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,17 @@ def packed_data(self):
143143
# Return the SCP header and the rest of the data
144144
return scp_header + self.data
145145

146+
def __repr__(self):
147+
"""Produce a human-redaable summary of (the most important parts of)
148+
the packet."""
149+
return ("<{} x: {}, y: {}, cpu: {}, "
150+
"cmd_rc: {}, arg1: {}, arg2: {}, arg3: {}, "
151+
"data: {}>".format(self.__class__.__name__,
152+
self.dest_x, self.dest_y, self.dest_cpu,
153+
self.cmd_rc,
154+
self.arg1, self.arg2, self.arg3,
155+
repr(self.data)))
156+
146157

147158
def _unpack_sdp_into_packet(packet, bytestring):
148159
"""Unpack the SDP header from a bytestring into a packet.

rig/machine_control/scp_connection.py

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def __new__(cls, x, y, p, cmd, arg1=0, arg2=0, arg3=0, data=b'',
5050
class SCPConnection(object):
5151
"""Implements the SCP protocol for communicating with a SpiNNaker chip.
5252
"""
53-
error_codes = {}
5453

5554
def __init__(self, spinnaker_host, port=consts.SCP_PORT,
5655
n_tries=5, timeout=0.5):
@@ -82,15 +81,6 @@ def __init__(self, spinnaker_host, port=consts.SCP_PORT,
8281
# Sequence values
8382
self.seq = seqs()
8483

85-
@classmethod
86-
def _register_error(cls, cmd_rc):
87-
"""Register an Exception class as belonging to a certain CMD_RC value.
88-
"""
89-
def err_(err):
90-
cls.error_codes[cmd_rc] = err
91-
return err
92-
return err_
93-
9484
def send_scp(self, buffer_size, x, y, p, cmd, arg1=0, arg2=0, arg3=0,
9585
data=b'', expected_args=3, timeout=0.0):
9686
"""Transmit a packet to the SpiNNaker machine and block until an
@@ -176,12 +166,13 @@ def send_scp_burst(self, buffer_size, window_size,
176166
class TransmittedPacket(object):
177167
"""A packet which has been transmitted and still awaits a response.
178168
"""
179-
__slots__ = ["callback", "packet", "n_tries",
169+
__slots__ = ["callback", "packet", "bytestring", "n_tries",
180170
"timeout", "timeout_time"]
181171

182172
def __init__(self, callback, packet, timeout):
183173
self.callback = callback
184174
self.packet = packet
175+
self.bytestring = packet.bytestring
185176
self.n_tries = 1
186177
self.timeout = timeout
187178
self.timeout_time = time.time() + self.timeout
@@ -226,12 +217,12 @@ def __init__(self, callback, packet, timeout):
226217
# expecting a response for it and can retransmit it if
227218
# necessary.
228219
outstanding_packets[seq] = TransmittedPacket(
229-
args.callback, packet.bytestring,
220+
args.callback, packet,
230221
self.default_timeout + args.timeout
231222
)
232223

233224
# Actually send the packet
234-
self.sock.send(outstanding_packets[seq].packet)
225+
self.sock.send(outstanding_packets[seq].bytestring)
235226

236227
# Listen on the socket for an acknowledgement packet, there may not
237228
# be one.
@@ -260,18 +251,19 @@ def __init__(self, callback, packet, timeout):
260251
consts.SDP_HEADER_LENGTH + 2)
261252

262253
# If the code is an error then we respond immediately
263-
if rc != consts.SCP_RC_OK:
264-
if rc in consts.SCP_RC_TIMEOUT:
254+
if rc != consts.SCPReturnCodes.ok:
255+
if rc in consts.RETRYABLE_SCP_RETURN_CODES:
265256
# If the error is timeout related then treat the packet
266257
# as though it timed out, just discard. This avoids us
267258
# hammering the board when it's most vulnerable.
268259
pass
269-
elif rc in self.error_codes:
270-
raise self.error_codes[rc]
271260
else:
272-
raise SCPError(
273-
"Unhandled exception code {:#2x}".format(rc)
274-
)
261+
# For all other errors, we'll just fall over
262+
# immediately.
263+
packet = outstanding_packets.get(seq)
264+
if packet is not None:
265+
packet = packet.packet
266+
raise FatalReturnCodeError(rc, packet)
275267
else:
276268
# Look up the sequence index of packet in the list of
277269
# outstanding packets. We may have already processed a
@@ -294,10 +286,13 @@ def __init__(self, callback, packet, timeout):
294286
# the given number of times then raise a timeout error for
295287
# it.
296288
if outstanding.n_tries >= self.n_tries:
297-
raise TimeoutError(self.n_tries)
289+
raise TimeoutError(
290+
"No response after {} attempts.".format(
291+
self.n_tries),
292+
outstanding.packet)
298293

299294
# Otherwise we retransmit it
300-
self.sock.send(outstanding.packet)
295+
self.sock.send(outstanding.bytestring)
301296
outstanding.n_tries += 1
302297
outstanding.timeout_time = (current_time +
303298
outstanding.timeout)
@@ -425,34 +420,46 @@ def seqs(mask=0xffff):
425420

426421

427422
class SCPError(IOError):
428-
"""Base Error for SCP return codes."""
429-
pass
423+
"""Base Error for SCP return codes.
430424
431-
432-
class TimeoutError(SCPError):
433-
"""Raised when an SCP is not acknowledged within the given period of time.
425+
Attributes
426+
----------
427+
packet : :py:class:`rig.machine_control.packets.SCPPacket`
428+
The packet being processed when the error occurred. May be None if no
429+
specific packet was involved.
434430
"""
435-
pass
436431

432+
def __init__(self, message, packet=None):
433+
self.packet = packet
434+
if self.packet is not None:
435+
message = "{} (Packet: {})".format(message, packet)
437436

438-
@SCPConnection._register_error(0x81)
439-
class BadPacketLengthError(SCPError):
440-
"""Raised when an SCP packet is an incorrect length."""
441-
pass
437+
super(SCPError, self).__init__(message)
442438

443439

444-
@SCPConnection._register_error(0x83)
445-
class InvalidCommandError(SCPError):
446-
"""Raised when an SCP packet contains an invalid command code."""
440+
class TimeoutError(SCPError):
441+
"""Raised when an SCP is not acknowledged within the given period of time.
442+
"""
447443
pass
448444

449445

450-
@SCPConnection._register_error(0x84)
451-
class InvalidArgsError(SCPError):
452-
"""Raised when an SCP packet has an invalid argument."""
453-
pass
446+
class FatalReturnCodeError(SCPError):
447+
"""Raised when an SCP command returns with an error which is connsidered
448+
fatal.
454449
450+
Attributes
451+
----------
452+
return_code : :py:class:`rig.machine_control.consts.SCPReturnCodes` or int
453+
The return code (will be a raw integer if the code is unrecognised).
454+
"""
455455

456-
@SCPConnection._register_error(0x87)
457-
class NoRouteError(SCPError):
458-
"""Raised when there is no route to the requested core."""
456+
def __init__(self, return_code=None, packet=None):
457+
try:
458+
self.return_code = consts.SCPReturnCodes(return_code)
459+
message = "RC_{}: {}".format(
460+
self.return_code.name.upper(),
461+
consts.FATAL_SCP_RETURN_CODES[self.return_code])
462+
except ValueError:
463+
self.return_code = return_code
464+
message = "Unrecognised return code {:#2X}."
465+
super(FatalReturnCodeError, self).__init__(message, packet)

tests/machine_control/test_packets.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from rig.machine_control.packets import SDPPacket, SCPPacket
22

3+
import sys
4+
35

46
class TestSDPPacket(object):
57
"""Test SDPPacket representations."""
@@ -263,3 +265,15 @@ def test_from_bytestring_2_args_short(self):
263265
# Check that the bytestring this packet creates is the same as the one
264266
# we specified before.
265267
assert scp_packet.bytestring == packet
268+
269+
def test_repr(self):
270+
"""Test the string representation of an SCP packet makes sense."""
271+
scp_packet = SCPPacket(dest_x=10, dest_y=20, dest_cpu=3,
272+
cmd_rc=2, arg1=123, arg2=456,
273+
data=b"foobar")
274+
# Note: Python 2 does not have the "b" prefix on byte strings
275+
assert repr(scp_packet) == (
276+
"<SCPPacket x: 10, y: 20, cpu: 3, "
277+
"cmd_rc: 2, arg1: 123, arg2: 456, arg3: None, "
278+
"data: {}'foobar'>".format(
279+
"b" if sys.version_info >= (3, 0) else ""))

tests/machine_control/test_scp_connection.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
import struct
55
import time
66

7-
from rig.machine_control.consts import SCPCommands, DataType, SDP_HEADER_LENGTH
7+
from rig.machine_control.consts import \
8+
SCPCommands, DataType, SDP_HEADER_LENGTH, RETRYABLE_SCP_RETURN_CODES, \
9+
FATAL_SCP_RETURN_CODES
810
from rig.machine_control.packets import SCPPacket
9-
from rig.machine_control.scp_connection import SCPConnection, scpcall
11+
from rig.machine_control.scp_connection import \
12+
SCPConnection, scpcall, FatalReturnCodeError
1013
from rig.machine_control import scp_connection
1114

1215

@@ -193,10 +196,11 @@ def mock_select(rlist, wlist, xlist, timeout):
193196
# times we specified.
194197
assert mock_conn.sock.send.call_count == mock_conn.n_tries
195198

196-
@pytest.mark.parametrize("err_code", [0x8b, 0x8c, 0x8d, 0x8e])
197-
def test_single_packet_fails_with_RC_P2P_ERROR(self, mock_conn, err_code):
199+
@pytest.mark.parametrize("err_code", RETRYABLE_SCP_RETURN_CODES)
200+
def test_single_packet_fails_with_retryable_error(self, mock_conn,
201+
err_code):
198202
"""Test correct operation for transmitting a single packet which is
199-
always acknowledged with one of the RC_P2P error codes.
203+
always acknowledged with one of the retryable error codes.
200204
"""
201205
# Create a packet to send
202206
packets = [scpcall(3, 5, 0, 12)]
@@ -344,14 +348,9 @@ def recv(self, *args):
344348
mock.patch("select.select", new=mock_select):
345349
mock_conn.send_scp_burst(512, 8, packets())
346350

347-
@pytest.mark.parametrize(
348-
"rc, error",
349-
[(0x81, scp_connection.BadPacketLengthError),
350-
(0x83, scp_connection.InvalidCommandError),
351-
(0x84, scp_connection.InvalidArgsError),
352-
(0x87, scp_connection.NoRouteError),
353-
(0x00, Exception)])
354-
def test_errors(self, mock_conn, rc, error):
351+
@pytest.mark.parametrize("rc",
352+
list(map(int, FATAL_SCP_RETURN_CODES)) + [0x00])
353+
def test_errors(self, mock_conn, rc):
355354
"""Test that errors are raised when error RCs are returned."""
356355
# Create an object which returns a packet with an error code
357356
class ReturnPacket(object):
@@ -374,7 +373,7 @@ def __call__(self, packet):
374373

375374
# Send an SCP command and check that the correct error is raised
376375
packets = [scpcall(3, 5, 0, 12)]
377-
with pytest.raises(error), \
376+
with pytest.raises(FatalReturnCodeError), \
378377
mock.patch("select.select", new=mock_select):
379378
mock_conn.send_scp_burst(256, 1, iter(packets))
380379

0 commit comments

Comments
 (0)