Skip to content

Conversation

@alexrudd2
Copy link
Collaborator

Closes #2716

alex@antorak:~/Desktop$ python3 client_test.py 1

--- Reading register 1 ---
Register 1 value: 17

# before change
alex@antorak:~/Desktop$ python3 client_test.py 99

--- Reading register 99 ---
Exception code 0x04
DEVICE FAILURE

# after change
alex@antorak:~/Desktop$ python3 client_test.py 99

--- Reading register 99 

Exception code 0x02
ILLEGAL DATA ADDRESS

if self.server.ignore_missing_devices:
return # the client will simply timeout waiting for a response
response = ExceptionResponse(self.last_pdu.function_code, ExceptionResponse.GATEWAY_NO_RESPONSE)
except KeyError:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not generic, problems in the sparse datastore, should be solved at that level.

keyError can probably mean different things in the different datastores.

Look at simulator.py around line 587, to see how it is handled there (apart from the ugly constant 2).

Copy link
Collaborator

@janiversen janiversen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am very mistaken, you make a lot of changes for something that is already implemented in the general datastore and used in the simulator datastore

However I might be wrong....but important is that datastore should handle all datastore problems and not pass the handling to the upper layer.

message = f"[No Such id] {string}"
ModbusException.__init__(self, message)

class NoSuchAddressException(ModbusException):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? That it something normal, that the library should handle gracefully (and it does in the simulator datastore).

Exception like e.g. NotImplementedException are there to signal the developers forgot to implement something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How else, besides bubbling an Exception up, is the response handler supposed to know what the underlying error was?

try:
return [self.values[i] for i in range(address, address + count)]
except KeyError as e:
raise NoSuchAddressException(str(e)) from e
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why raise an exception, that is not the pattern we normally use....exceptions are raised when the library encounters a problem that was not covered by code.

You can just return ExceptionResponse.ILLEGAL_ADDRESS, that should work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getValues (in the happy case) returns a list of values, not a full ModbusResponse.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look in simulator.py line 588, it returns a list of values or an integer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!! Returning a magic value is strange, undocumented, and causing confusing types.

Arguably self.validate() itself should throw the Exception.

if self.server.ignore_missing_devices:
return # the client will simply timeout waiting for a response
response = ExceptionResponse(self.last_pdu.function_code, ExceptionResponse.GATEWAY_NO_RESPONSE)
except NoSuchAddressException:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request handler should really not know about the datastore...it handles requests and returns a response.

@alexrudd2
Copy link
Collaborator Author

Unless I am very mistaken, you make a lot of changes for something that is already implemented in the general datastore and used in the simulator datastore

However I might be wrong....but important is that datastore should handle all datastore problems and not pass the handling to the upper layer.

At least for the sparse datastore, there is an uncaught KeyError in get_values() that bubbles all the way up to the request handler before being caught by except Exception.

except Exception as exc: # pylint: disable=broad-except
Log.error(
"Datastore unable to fulfill request: {}; {}",
exc,
traceback.format_exc(),
)
response = ExceptionResponse(self.last_pdu.function_code, ExceptionResponse.DEVICE_FAILURE)

This exception handler is overly generic. (Indeed, even pylint complains).
In this case, it can only send ExceptionResponse.DEVICE_FAILURE even if we "know" it should be ExceptionResponse.ILLEGAL_ADDRESS.

@janiversen
Copy link
Collaborator

First of all sparse data block is an old relic, that is going to be removed as soon as possible...but anyhow the key error should be caught in the datastore not passed along.

The overly generic exception handler is quite correct, that was made like this to ensure that the server return a valid frame when it encounters a programming problem....you are trying to add a perfectly valid scenario (illegalAddress) to that. Datastore problems should be handled in the datastore module.

@alexrudd2
Copy link
Collaborator Author

alexrudd2 commented Aug 8, 2025

.you are trying to add a perfectly valid scenario (illegalAddress) to that.

The entire purpose of this PR is to close #2716.

Any solution which replies with 0x04 Device Failure instead of 0x02 Illegal Address does not accomplish that.
EDIT: Reversed the order

@alexrudd2
Copy link
Collaborator Author

First of all sparse data block is an old relic, that is going to be removed as soon as possible

Noted, I'm expecting to have to refactor my downstream code again for 4.0

@janiversen
Copy link
Collaborator

.you are trying to add a perfectly valid scenario (illegalAddress) to that.

The entire purpose of this PR is to close #2716.

Any solution which replies with 0x04 Device Failure instead of 0x02 Illegal Address does not accomplish that. EDIT: Reversed the order

The solution is to do as the datastore simulator does, I even gave you the line number.

@alexrudd2
Copy link
Collaborator Author

The solution is to do as the datastore simulator does, I even gave you the line number.

Jan, I very much appreciate your suggestions for better code, and I'm not trying to argue for its own sake. I urge you to reconsider.

async def async_getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int] | list[bool] | int:
"""Get `count` values from datastore.
:param fc_as_hex: The function we are working with
:param address: The starting address
:param count: The number of values to retrieve
:returns: The requested values from a:a+c
"""
return self.getValues(fc_as_hex, address, count)

def getValues(self, func_code, address, count=1):
"""Return the requested values of the datastore.
:meta private:
"""
if not self.validate(func_code, address, count):
return 2
result = []

With all due respect, this code is bad.

  • Returning magic numbers for an error condition is not typical Python at all; it is more common to use Exceptions.

  • It's not documented in either get_values() or async_getValues(). Even if it were, what would the docstring be? """"Return the requested values of the datastore, or '2' if the values cannot be found in the datastore.""" 🤮

pyright complains:
image
mypy would complain as well, if the function were typed.
image

  • The code doesn't even work! The simulator generates inscrutable error message when accessing an invalid register.
2025-08-08 14:39:18,050 ERROR requesthandler:108 Datastore unable to fulfill request: object of type 'int' has no len(); Traceback (most recent call last):
  File "/home/alex/git/pymodbus/pymodbus/server/requesthandler.py", line 100, in handle_request
    response = await self.last_pdu.update_datastore(context)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alex/git/pymodbus/pymodbus/pdu/register_message.py", line 47, in update_datastore
    return response_class(
           ^^^^^^^^^^^^^^^
  File "/home/alex/git/pymodbus/pymodbus/pdu/pdu.py", line 37, in __init__
    self.count: int = count or len(self.registers)
                               ^^^^^^^^^^^^^^^^^^^
TypeError: object of type 'int' has no len()
  • The poor client is left with 0x04 DEVICE_FAILURE. I refer to pages 47-48 available of V1_1b3. 0x04 is "An unrecoverable error occurred while the server was attempting to perform the requested action." That's not true - the error is recoverable.
    The spec is clear that 0x02 should be sent in this case: "If the output address is non–existent in the server device, the server will return the exception response with the exception code shown (02). This specifies an illegal data address for the server."

@janiversen
Copy link
Collaborator

You are, as usual right, in a lot of your rant.....BUT at the end of the day there are a couple of design goals which are final:

  • A module handles it's own problems (this is mandatory)
  • Exceptions are only raised in situations where the developer have made something wrong (this can be a bit flexible but within the module).

Simulator.py do return IllegalAddress, but might break afterwards (at least one user is satisfied with the current solution)....however the datastore are moving in the direction of returning a list is ok and an integer if not ok == modbus exception.

Sequential and sparse data block are being changed to convert internally to the simulator module (which is not the simulator datastore). It means in the future it is not recommended to inherit the datastore and make your own version, but instead define a dict to run with the simulator.

FYI, in the future we will have a server with a relative fixed setup and thus easier to use, and a simulator with a lot more flexibility (including a http interface if activated).

@alexrudd2
Copy link
Collaborator Author

Simulator.py do return IllegalAddress,

No, it does not, or at least not to modbus requests. See my test code - it returns DeviceFailure. Perhaps you're thinking of the HTTP / webpage frontend?

Anyways, to summarize the options I see:
(1) datastores sometimes return different values, request handler fails ==> buggy and needs to be improved
(2) datastores throw exceptions, request handler catches them (BAFP) ==> my recommendation, but rejected for design reasons
(3) request handler checks for any possible datastore errors (LBYL) ==> should work, most maintains existing call signatures
(4) datastore return additional status (error/ok), request handler parses ==> not my favorite, but could also work

@janiversen
Copy link
Collaborator

The new datastore goes in direction of 1), but of course you are welcome to suggest other routes, as long as datastore handling stays within the datastore module.

@alexrudd2
Copy link
Collaborator Author

alexrudd2 commented Aug 9, 2025

Closing in favor of #2733 (which went with option 4)

@alexrudd2 alexrudd2 closed this Aug 9, 2025
@janiversen janiversen deleted the issue-2716 branch September 10, 2025 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lack of IllegalAddress error while reading from not existing holding registers in SparseDataStore

3 participants