Skip to content

The review of Evok 3.1.0: a failed step, the watchdog timeout, the alias before the write - #280

Merged
miroslav-ondra merged 5 commits into
dev-3.1from
review-276-fixes
Oct 26, 2026
Merged

miroslav-ondra merged 5 commits into
dev-3.1from
review-276-fixes

Conversation

@miroslav-ondra

@miroslav-ondra miroslav-ondra commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

The findings of the review of #276 (Evok 3.1.0), fixed in dev-3.1 before the release.

Changes

A failed end of a pulse left the output on (blocks/sequencer.py)

  • The last step of a sequence ended it before its write. When the write failed (a noise or a timeout on RS485, an error of the unit), the sequence was gone, nothing wrote the end again and the relay stayed on; finish_all() at the shutdown did not write it either. A failed middle step lost the final of a sequence the same way.
  • A step failed by an error of the communication (UnitCommunicationError, TModbusError, TimeoutError, OSError) is written again until it succeeds or the sequence is cancelled or replaced, the sequence stays running meanwhile. The delay starts at STEP_RETRY_DELAY (1 s) and doubles up to STEP_RETRY_MAX_DELAY (30 s), a missing unit does not load its bus every second. The failure is logged, again when the kind of the error (its root cause) changes and every 5 minutes, a later success too.
  • A step refused by the unit (an exception response other than Server Device Busy, Acknowledge or of a gateway, e.g. Illegal Data Value or Server Device Failure; an invalid request) or another error ends the sequence and is logged; written again it would be refused forever and the output refused the changes.
  • The final state of such a sequence (if it has one) is written as its last step while it still runs: a change of the output meanwhile is refused (409), finish_all() waits for it, an error of the communication writes it again; it is not written by a task the sequence no longer owns, nor when it is the refused step itself (the end of a pulse).
  • The step is done (step of the state) after its successful write; the last step ends the sequence after it (under the lock of the unit, the state read after it is not running).
  • finish_all() waits for the steps being written, then writes the final state of the sequences whose last step was not written (Running.finished): a failed end of a pulse while Evok stops is written by the final state.

Watchdog (modbus/special.py)

  • A changed timeout is an event, the clients kept the old one.
  • timeout, value and reset are checked before any change, timeout must be a whole number of ms: {"value": 1, "timeout": "1.5"} enabled the watchdog and set the alias before int() failed (400 after a partial change), 2.9 was cut to 2, an invalid value or reset set the alias or wrote the registers before; reset of a unit without the reset coil is ignored as before.

An invalid alias after the write (modbus/base.py, devices.py)

  • set_devices() checks the aliases of all assignments (Devices.check_alias()) before the first write, in the order of the changes (by the units), with the aliases released and taken by the earlier assignments. A used, invalid, reserved alias or one equal to a circuit switched the output and then the request failed with 400; the same alias twice in one bulk is refused, an alias moved to another device in one bulk works; an alias which is not a string (0, false, a list) is refused as invalid; alias: null is not given, the alias is kept. All APIs set the devices by set_devices(); set_alias() uses the same check.

AccessorBit writes with its pin (#279), a bit accessor with pin=False was pinned.

Docs: the retried step in sequencer.md and the API changes, the watchdog timeout, the alias.

Testing

  • pytest tests: 1302 passed; new tests: alias: null keeping the alias, the refused end of a pulse sent once, reset of a watchdog without the coil, the final state written while the sequence runs (also again after a noise), finish_all() waiting for it, Server Device Failure refused and Server Device Busy retried, an error with a changing text logged once, a step refused by the unit ending the sequence with its final state, the doubling delay, the changed error logged, the invalid watchdog value and reset, an alias which is not a string, the failed step written again and logged once, a refused step ending the sequence, the step done after its write, the last step ending the sequence after its write, the failed end of a DO pulse written again, the final state after a failed last step at the shutdown, the same alias twice in a request, an alias moved in a request, an alias released by a later unit, the watchdog timeout as an event, the invalid timeouts (1.5, 2.9, -1, x) without any change, a whole timeout as a string, the invalid aliases before the write, the bit without a pin. Each fails without its fix. ruff check and ruff format --check src clean.
  • Patron M527 (192.168.221.159): a pulse of 0.5 s of led ext2_01; {"value": 1, "alias": "tmpx"} with tmpx used by ext2_02 is 400 and the LED stays off; wd ext3 {"value": 1, "timeout": "1.5"} is 400 and the watchdog stays disabled with its timeout; a bulk moving the alias pump from led ext2_02 to led ext2_03 succeeds; wd ext3 {"alias": "wdx", "value": "maybe"} is 400 without the alias.

🤖 Generated with Claude Code

miroslav-ondra and others added 5 commits October 26, 2026 22:49
…ias before the write

- sequencer: a failed step is written again every STEP_RETRY_DELAY (1 s) until
  it succeeds or the sequence is cancelled, it is logged once; the last step
  ends the sequence after its successful write. A failed end of a pulse (e.g.
  a noise on the bus) ended the sequence before the write, the relay stayed on
  and finish_all() did not write it either. finish_all() does not write the
  final state of a sequence writing its last step again
- Watchdog: a changed timeout is an event; the timeout is checked before any
  change, a whole number of ms (it was cut by int()), an invalid one enabled
  the watchdog and set the alias before the error
- set_devices() checks the aliases of the assignments before the first write
  (Devices.check_alias()): the output was switched and then the request failed
  on a used, invalid or reserved alias
- AccessorBit writes with its pin
- docs: the retried step, the watchdog timeout, the alias in the API changes
  and sequencer.md
- tests: the retried step and the end after the write, the failed end of a DO
  pulse written again, the watchdog timeout, the alias, the bit without a pin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… errors, the aliases in order

- finish_all() writes the final state of a sequence whose last step was being
  written and failed (Running.finished after the successful write), it left
  the relay on
- only the errors of the communication are written again
  (UnitCommunicationError, TModbusError, TimeoutError, OSError), another
  error ends the sequence and is logged; a value refused by the output was
  written again forever and the output refused every change
- the step is done after its successful write, a step written again was
  reported as done
- set_devices() checks the aliases in the order of the changes (by the units),
  with the aliases released and taken by the earlier assignments: the same
  alias twice in one request passed the check, an alias moved to another
  device was refused
- set_alias() checks the alias by check_alias(), the rules in one place
- docs, tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ams, a non-string alias

- a step refused by the unit (an exception response Illegal Function, Data
  Address or Data Value, an invalid request) is not written again, it was an
  error of the communication (TModbusError) and was retried forever
- a step which ends its sequence by an error writes the final state of the
  sequence, the output stayed in its state
- the delay of a failed step doubles from 1 s up to 30 s, a missing unit does
  not load its bus every second; the failure is logged again when the error
  (with its cause) changes and every 5 minutes
- Watchdog.set() checks value and reset before any change too, an invalid one
  set the alias, enabled the watchdog or wrote the timeout before the error
- an alias which is not a string (0, false) is refused as invalid before the
  write, it passed as a reset and failed by TypeError after the write
- docs, tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on responses, the alias type

- the final state after a refused step is written as the last step of the
  sequence while it runs: it ended before, a new pulse or a change in the gap
  was overwritten by the old final state and finish_all() did not wait for it;
  an error of the communication writes it again, a noise lost it
- every exception response of the unit is a refused request (e.g. Server Device
  Failure was retried forever) except Server Device Busy, Acknowledge and the
  errors of a gateway, which are written again
- a failed step is logged again when the kind of its error changes, the text
  of a timeout differs by each attempt and was logged each time
- _check_aliases() checks the alias by check_alias() before the taken ones, a
  list alias raised TypeError (500)
- docs: the final state only of a sequence which has one, the watchdog timeout
  is cut to 65535
- tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… without the coil

- _check_aliases() skips an alias null as not given: it was checked as
  a reset and released the alias for another device, set_alias() keeps it and
  the duplicate failed after the write
- the final state after a refused step is not written when it is the refused
  step itself, e.g. the end of a pulse was sent again and logged twice
- Watchdog.set() ignores reset of a unit without the reset coil as before,
  an invalid one was refused
- docs: the PRs of the new entries of the API changes
- tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@miroslav-ondra
miroslav-ondra merged commit af94724 into dev-3.1 Oct 26, 2026
5 checks passed
@miroslav-ondra
miroslav-ondra deleted the review-276-fixes branch October 26, 2026 22:29
miroslav-ondra added a commit that referenced this pull request Oct 26, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@miroslav-ondra miroslav-ondra mentioned this pull request Oct 26, 2026
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.

1 participant