62 Commits

Author SHA1 Message Date
Sven Sager
f3ffdd8eaa fix(io): Fix ios1 dbus policy
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-03-11 15:12:45 +01:00
Sven Sager
f4eff16ab6 refactor: Update type annotations
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-27 15:42:15 +01:00
Sven Sager
d95a65c18c test(dbus): Update bus_address for BusProvider clases
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-27 15:42:15 +01:00
Sven Sager
2b22c103c8 fixup: Integrate NotifyDriverReset handling for ios1
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-27 15:42:15 +01:00
Sven Sager
794089a5b5 refactor: Simplify D-Bus handling by using address-based connections
Replaced direct `SystemBus` and `SessionBus` instances with
address-based D-Bus connections. BusProviders will open their own bus
connections and close it at the end.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-27 15:39:02 +01:00
Sven Sager
5dfd8b9f2f feat: Add published event to bus providers for thread synchronization
Introduced a threading `Event` named `published` in `bus_provider_ios1`
and `bus_provider_middleware1` modules to signal successful D-Bus
publishing.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 18:52:29 +01:00
Sven Sager
4ea8d6bc9a feat: Add monitoring and automatic restart for bus provider threads
Implemented checks in `MiddlewareDaemon` to monitor the status of
`bp_middleware1` and `bp_ios1` threads. Added automatic restarts for
non-alive threads using `dbus_start`.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 18:23:55 +01:00
Sven Sager
fdaf7b3ac6 refactor: Restructure and rename bus provider modules and classes
Renamed and moved `BusProvider` and `BusProviderIo` to
`BusProviderMiddleware1` and `BusProviderIos1` respectively for better
clarity and modular organization. Updated all related imports and
references accordingly.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 18:23:01 +01:00
Sven Sager
3dbad93975 feat: Add bus_filter to dbus_stop for selective stopping
Enhanced the `dbus_stop` method in `MiddlewareDaemon` to support an
optional `bus_filter` parameter, allowing selective stopping of bus
providers. Updated imports to include `List` from `typing`.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 18:23:01 +01:00
Sven Sager
cf90b2dd55 refactor: Rename bus provider attributes for clarity
Replaced `bus_provider` and `io_bus_provider` with `bp_middleware1` and
`bp_ios1` to improve naming consistency and readability in
`MiddlewareDaemon`.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 18:23:01 +01:00
Sven Sager
4bd8fa74a9 refactor: Add helper functions for D-Bus connections and bus handling
Replaced direct D-Bus initialization with
`get_new_system_dbus_connection` and `get_new_session_dbus_connection`
helper functions. This functions will create a new connection to use
separate busses.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 17:25:51 +01:00
Sven Sager
bf5bef114c chore: Reformat with black
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 16:53:02 +01:00
Sven Sager
2c08829a13 feat(io): Replace property with method to get IOs of a device
Introduced methods for retrieving input and output object paths in the
`InterfaceDevice` class. Removed `inp` and `out` properties in favor of
these methods.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 08:59:36 +01:00
Sven Sager
ce1a25ac0e feat(io): Add EmitsChangedSignal annotations to device properties
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 08:50:02 +01:00
Sven Sager
f387b0a836 refactor(io): Rename GetIO method to GetByName in IO manager
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 08:43:11 +01:00
Sven Sager
10ad65a62c refactor(io): Update get_*_object_path to accept objects instead
Modified `get_io_object_path` and `get_device_object_path` functions to
accept full `IOBase` and `Device` objects instead of string identifiers.
All properties are available in case of changing the object path format.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 08:41:29 +01:00
Sven Sager
0e69ef432b refactor(io): Simplify InterfaceDeviceManager and device handling
Streamlined device management by introducing a dedicated `object_path`
property in `InterfaceDevice`. Refactored `InterfaceDeviceManager` to
use this property and accept a list of `InterfaceDevice` instances,
removing direct `RevPiModIO` dependencies. Updated D-Bus publishing to
reflect the new structure.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-06 08:36:39 +01:00
Sven Sager
35dbed0798 refactor(io): Rename Get method to GetIO in IOManager interface
Updated method name in D-Bus interface and backend to do not use the
same name as org.freedesktop.DBus.Properties. Som libs will map all
methods of a node to an object for direct calling. If the names are the
same, you have to filter the interface.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-05 16:23:27 +01:00
Sven Sager
1842bbd2f5 feat(io): Emit property change signals for byteorder and signedness
Added `PropertiesChanged` signals for `byteorder` and `signed` property
updates. Updated D-Bus interface annotations to specify change signal
behavior for properties. Simplified `emit_io_change` implementation.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-05 16:12:15 +01:00
Sven Sager
3d64cd5837 refactor(io): Simplify generation of io object path
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-05 16:11:32 +01:00
Sven Sager
5528f6dff4 doc(io): Update io_controller.py example file
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 16:06:48 +01:00
Sven Sager
6372ed4e7f feat(io): Add Set* (Byteorder, Signed, Value) methods to IO interface
Introduced `SetByteorder` and `SetSigned` methods in both the `Input`
and `Output` D-Bus interfaces, and added the `SetValue` method
specifically for the `Output` interface. Implemented corresponding
methods in the backend.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
710d7b2078 feat(io): Add GetByName and GetByPosition methods to DeviceManager
Introduced `GetByName` and `GetByPosition` methods in the D-Bus
interface and implementation for retrieving device object paths by name
or position. Updated `GetAllDevices` return type to `ao`.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
8ee5e0836f feat(io): Add position property to InterfaceDevice
Introduced a new `position` property with read access in the D-Bus
interface and implemented its corresponding method.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
6f1bd7f7ea refactor(io): Rename IO event methods to signal methods for clarity
Renamed `ActivateIoEvents` to `ActivateIoSignals` and
`DeactivateIoEvents` to `DeactivateIoSignals` in both the D-Bus
interface and implementation to better align with D-Bus signal
semantics.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
785569c86b fix(io): Do not read/wirte process image while device is in self-update
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
415e8502f7 fix(io): Handle large integer values with fallback to raw byte arrays
Added a fallback mechanism to handle cases where integer values exceed
valid range by using raw byte arrays (`ay`). Updated D-Bus methods and
properties to support this behavior.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
b16331cf84 feat(io): Add bitaddress property to IO interfaces
Introduced the `bitaddress` property with read-write access to `Input`
and `Output` D-Bus interfaces. Implemented corresponding property
methods.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
a2d1531e77 feat(io): Simplify IO interface hierarchy and unify property handling
Replaced specific IO interface classes (`InterfaceInpBool`,
`InterfaceInpInt`, etc.) with generic `InterfaceInput` and
`InterfaceOutput` classes to simplify the hierarchy.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-04 15:20:34 +01:00
Sven Sager
bbbbd3e0e1 fix(io): Add argument names to D-Bus methods for input/output paths
Updated `GetAllInputs` and `GetAllOutputs` methods in the D-Bus
interface to include explicit argument names (`object-path-list`) and
adjusted their types to `ao` not strings.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-03 08:54:56 +01:00
Sven Sager
a43fc764c6 doc: Add D-Bus data types documentation
Added a new `docs/dbus.md` file with an overview of standardized D-Bus
data types and their signatures. Updated `README.md` with a reference to
the documentation.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-03 08:03:37 +01:00
Sven Sager
0716169a03 feat(io): Add min_value and max_value properties to IOs
Added `min_value` and `max_value` properties to integer IOs for
determining value ranges. Updated D-Bus interface with corresponding
properties.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-03 08:03:37 +01:00
Sven Sager
77a3fbb16c feat(io): Add inp and out properties to InterfaceDeviceManager
Added `inp` and `out` properties for accessing input and output paths in
`InterfaceDeviceManager`. Updated D-Bus interface with corresponding
properties.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-02 17:06:34 +01:00
Sven Sager
037e9c05ac style(io): Reformatted with black
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-02 17:06:34 +01:00
Sven Sager
66c8c9851b refactor(io): Replace _get_io_path method with get_io_path utility
The `_get_io_path` method was removed from `InterfaceIoManager` and
replaced with the `get_io_path` utility function from `ios1_helper.py`
to reduce redundancy and improve code reusability.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-02 17:06:34 +01:00
Sven Sager
afdae78336 feat(io): Add new IO properties and corresponding methods
Added `address`, `byteorder`, and `length` properties to D-Bus
interfaces for IOs. Implemented corresponding property methods in
`ios1_helper.py`.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-02-02 17:06:33 +01:00
Sven Sager
4dd4c814b5 fix(io): Add property methods on integer IOs
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-01-30 13:26:21 +01:00
Sven Sager
b8b99ed3b0 feat(io): Add InterfaceDevice class for device handling via D-Bus
Introduced the `InterfaceDevice` class, enabling detailed device
property access through D-Bus. Updated `bus_provider_io` to include
device interfaces in the D-Bus provider.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-01-30 13:06:15 +01:00
Sven Sager
b68f1ffb36 feat(io): Add InterfaceDeviceManager to D-Bus bus provider
Integrated the InterfaceDeviceManager class into the com.revolutionpi
.ios1 D-Bus bus provider. This addition enables managing device paths
and querying all devices via D-Bus.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-01-30 12:29:08 +01:00
Marc Kittner
ffd04a0410 fix: Use variant_type instead of hard coded boolean
Signed-off-by: Marc Kittner <m.kittner@kunbus.com>
2026-01-30 11:26:15 +01:00
Sven Sager
549cddf594 feat(io): Integrate com.revolutionpi.ios1 bus to daemon
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-01-16 14:27:41 +01:00
Sven Sager
f56db7ab1c doc(io): Add IO example program
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-01-16 14:27:41 +01:00
Sven Sager
208b391aa9 feat(io): Add dbus policy for com.revolutionpi.ios1 bus
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-01-16 14:27:41 +01:00
Sven Sager
917ab7ab1f feat(io): Add dbus bus com.revolutionpi.ios1
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2026-01-16 14:27:41 +01:00
Sven Sager
b4f817b477 feat(examples): Add revpi-config CLI tool for RevPi configuration
Introduced a new Python-based command-line tool `revpi-config` to manage
Revolution Pi features via D-Bus. The tool supports enabling,
disabling, checking status, and availability for various features.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-06-04 08:10:52 +02:00
Sven Sager
66046b6a9d feat(dbus): Update systemd unit state check to include 'activating'
Previously, the check only considered 'active' as a valid ActiveState.
This update ensures the system also accounts for units in the
'activating' state, improving compatibility with transitional states in
systemd services.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 14:54:44 +02:00
Sven Sager
9ec1e04d2b feat(dbus): Update service key for systemd-timesyncd to 'ntp'
Renamed the key 'systemd-timesyncd' to 'ntp' in the services mapping.
This improves naming consistency and aligns it with the expected service
identifier. No functional changes were introduced.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 14:54:14 +02:00
Sven Sager
1cb1062138 feat(dbus): Update feature key from 'dphys-swapfile' to 'swapfile'
Renamed the feature key for swapfile configuration to ensure consistency
and simplify naming.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 14:28:07 +02:00
Sven Sager
8d01eee66d feat(dbus): Update status change notification in feature toggle methods
Added calls to `StatusChanged` in `Disable` and `Enable` methods to
notify when a feature's state is updated. This ensures proper tracking
and external communication of feature status changes.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 14:28:07 +02:00
Sven Sager
40059d6068 feat(dbus): Add InterfaceSoftwareServices to bus provider initialization
This change integrates the InterfaceSoftwareServices class into the list
of interfaces initialized by the bus provider. It ensures the new
interface is properly registered and available for use, improving
system functionality and extensibility.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 14:18:37 +02:00
Sven Sager
50789853e9 feat(dbus): Add D-Bus interface for managing software services
Introduce `InterfaceSoftwareServices` to handle service enable/disable
actions, status, and availability via D-Bus. Consolidate `avahi` service
configuration into the new interface by removing redundant logic from
`revpi_config.py`.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 14:18:37 +02:00
Sven Sager
d2e2f1c7f3 refactor(dbus): Move simple_systemd to systemd_helper
Moved the `simple_systemd` function to a dedicated `systemd_helper`
module to improve code organization and reusability. Updated imports
accordingly in affected files.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 14:18:37 +02:00
Sven Sager
32574e0815 refactor: DBus interface includes bus instance
Updated `InterfaceRevpiConfig` to require a bus parameter, ensuring
proper initialization. Introduced a constructor in `DbusInterface` to
store the bus instance.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 12:37:15 +02:00
Sven Sager
a29979ad78 feat(dbus): Add AvailabilityChanged signal to .RevpiConfig
This signal provides a mechanism for notifying changes in availability
status. It will complement the existing `StatusChanged` signal and
enhance the system's event-handling capabilities.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-27 07:58:40 +02:00
Sven Sager
431f9308de feat(dbus): Add StatusChanged DBus signal to interface configuration
Introduced the `StatusChanged` signal in `.RevpiConfig` interface to
notify changes in feature status. Emitting this signal in `Enable` and
`Disable` methods ensures real-time updates for feature state changes.
This enhances communication and monitoring within the DBus middleware.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-26 15:21:08 +02:00
Sven Sager
dc2d1ab3a0 ci: Test in a bookworm container
In order to be able to provide all functions of the DBus during testing,
the CI uses the Python 3.11 version in a Bookworm container for
testing. This is the version that is pre-installed in Debian Bookworm.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-23 09:54:30 +02:00
Sven Sager
983c6cefea ci: Start dbus session bus for testing
A dbus session bus must be started for testing. The command
`dbus-run-session` creates a session for the following command. As a
result, `pytest` runs with its own session bus for the tests.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-23 09:54:30 +02:00
Sven Sager
a977884e17 ci: Set machine-id for dbus
Systemd does not exist in the docker file. So we just set an machine-id,
which the dbus-daemon can use to start a session.

Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-23 09:54:30 +02:00
Sven Sager
20fb85a3f0 ci: Install system dependencies for testing
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-23 09:52:43 +02:00
Sven Sager
f4efb1daae fix: Add missing reuse license information
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-23 09:36:41 +02:00
Sven Sager
fb181f8810 ci: Add Revolution Pi pipelines
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-22 13:14:22 +02:00
Sven Sager
3dee7784e2 ci: Add GitLab python pipelines
Signed-off-by: Sven Sager <s.sager@kunbus.com>
2025-05-22 13:14:22 +02:00
41 changed files with 1348 additions and 482 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH <support@kunbus.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

40
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH <support@kunbus.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
default:
tags:
- self-hosted
- host-arm64
- high-perf
include:
- project: "revolutionpi/infrastructure/ci-templates"
file: "base.yml"
- project: "revolutionpi/infrastructure/ci-templates"
file: "check-commit/lint-commit.yml"
- project: "revolutionpi/infrastructure/ci-templates"
file: "reuse-lint.yml"
- project: "revolutionpi/infrastructure/ci-templates"
file: "package-devel.yml"
- local: debian/gitlab-ci.yml
rules:
- exists:
- debian/gitlab-ci.yml
run_tests:
stage: test
image: python:3.11-bookworm
script:
- apt-get update
- apt-get -y install dbus libgirepository1.0-dev
- dbus-uuidgen --ensure=/etc/machine-id
- pip install -r requirements.txt
- PYTHONPATH=src dbus-run-session -- pytest -v --junitxml=report.xml --cov=src --cov-report term --cov-report xml:coverage.xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
reports:
junit: ${CI_PROJECT_DIR}/report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml

View File

@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH <support@kunbus.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
recursive-include .reuse * recursive-include .reuse *
recursive-include data * recursive-include data *
recursive-include LICENSES * recursive-include LICENSES *

View File

@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH <support@kunbus.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
SHELL := bash SHELL := bash
MAKEFLAGS = --no-print-directory --no-builtin-rules MAKEFLAGS = --no-print-directory --no-builtin-rules
.DEFAULT_GOAL = all .DEFAULT_GOAL = all

View File

@@ -1,3 +1,14 @@
<!--
SPDX-FileCopyrightText: 2025 KUNBUS GmbH <support@kunbus.com>
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Middleware for Revolution Pi # Middleware for Revolution Pi
This middleware will support D-Bus as IPC interface. This middleware will support D-Bus as IPC interface.
## D-Bus
See [docs/dbus.md](docs/dbus.md) for an overview of the standard D-Bus data
types.

View File

@@ -9,10 +9,7 @@
<!-- System group picontrol --> <!-- System group picontrol -->
<policy group="picontrol"> <policy group="picontrol">
<allow send_destination="com.revolutionpi.ios1" <allow send_destination="com.revolutionpi.ios1"/>
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="com.revolutionpi.ios1"
send_interface="com.revolutionpi.middleware1.PiControl"/>
</policy> </policy>
<!-- Standard-Policy --> <!-- Standard-Policy -->

View File

@@ -1,40 +1,59 @@
from time import time # -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""Switches 14 outputs on a DIO to the same value as the first input."""
from time import perf_counter
from gi.repository import GLib from gi.repository import GLib
from pydbus import SystemBus from pydbus import SystemBus
detected_signal = False detected_signal = False
timestamp = time() timestamp = perf_counter()
# Get system bus
bus = SystemBus() bus = SystemBus()
bus_revpi = bus.get("com.revolutionpi.ios1", "/com/revolutionpi/ios1")
interface = bus_revpi["com.revolutionpi.ios1.IoManager"]
io_o_1_path = interface.Get("O_1") # Get IoManager interface on ios1 bus
io_revpiled_path = interface.Get("RevPiLED") iface_io_manager = bus.get(
"com.revolutionpi.ios1",
"/com/revolutionpi/ios1",
)["com.revolutionpi.ios1.IoManager"]
io_RevPiLED = bus.get("com.revolutionpi.ios1", io_revpiled_path) # Query object path of RevPiLED output
io_O_1 = bus.get("com.revolutionpi.ios1", io_o_1_path) path_revpi_led = iface_io_manager.Get("RevPiLED")
# Get Output interface in the queried object path.
out_RevPiLED = bus.get("com.revolutionpi.ios1", path_revpi_led)["com.revolutionpi.ios1.Output"]
# Get all 14 outputs
lst_path_outputs = [iface_io_manager.Get(f"O_{i}") for i in range(1, 15)]
lst_out_outputs = [
bus.get("com.revolutionpi.ios1", path)["com.revolutionpi.ios1.Output"]
for path in lst_path_outputs
]
def signal_handler(io_name, io_value): def signal_handler(io_name, io_value):
global timestamp global timestamp
print(f"Signal received: {io_name} = {io_value}") print(f"Signal received: {io_name} = {io_value}")
if io_name == "O_1":
timestamp = time() if io_name == "O_14":
io_RevPiLED.Set("com.revolutionpi.ios1.OutInt", "value", GLib.Variant("i", int(io_value))) print(f"Time since input detection: {perf_counter() - timestamp}")
elif io_name == "I_1": elif io_name == "I_1":
io_RevPiLED.Set("com.revolutionpi.ios1.OutInt", "value", GLib.Variant("i", int(io_value))) timestamp = perf_counter()
io_O_1.Set("com.revolutionpi.ios1.OutBool", "value", GLib.Variant("b", not io_value))
print(time() - timestamp) out_RevPiLED.SetValue(GLib.Variant("i", io_value))
for output in lst_out_outputs:
output.SetValue(GLib.Variant("b", io_value))
# Start event detection # Start change detection to fire signals on dbus
interface.ActivateIoEvents() iface_io_manager.ActivateIoSignals()
interface.onIoChanged = signal_handler iface_io_manager.onIoChanged = signal_handler
try: try:
loop = GLib.MainLoop() loop = GLib.MainLoop()
@@ -42,5 +61,5 @@ try:
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
# Stop event detection # Stop change detection
interface.DeactivateIoEvents() iface_io_manager.DeactivateIoSignals()

51
docs/dbus.md Normal file
View File

@@ -0,0 +1,51 @@
<!--
SPDX-FileCopyrightText: 2026 KUNBUS GmbH <support@kunbus.com>
SPDX-License-Identifier: GPL-2.0-or-later
-->
# D-Bus
## Datentypen
Hier ist eine **Liste der standardisierten DBus-Datentypen** (Signaturen), wie
sie u. a. in Introspection/XML (`type="..."`) verwendet werden.
## Basis-Typen (Basic Types)
| Signatur | Bedeutung |
|----------|-------------------------------------|
| `y` | Byte (unsigned 8-bit) |
| `b` | Boolean |
| `n` | Int16 (signed) |
| `q` | UInt16 (unsigned) |
| `i` | Int32 (signed) |
| `u` | UInt32 (unsigned) |
| `x` | Int64 (signed) |
| `t` | UInt64 (unsigned) |
| `d` | Double (IEEE 754) |
| `s` | String (UTF8) |
| `o` | Object Path |
| `g` | Signature (Typ-Signatur als String) |
## Container-Typen (Compound/Container)
| Form | Bedeutung | Beispiel |
|------------|-------------------------------------------------|---------------------------------|
| `aX` | Array von `X` | `as` = Array von Strings |
| `(XYZ...)` | Struct/Tupel | `(is)` = (Int32, String) |
| `{KV}` | Dict-Entry (nur innerhalb eines Arrays erlaubt) | `a{sv}` = Dict String → Variant |
## Spezial-Typ
| Signatur | Bedeutung |
|----------|---------------------------------------------------|
| `v` | Variant (beliebiger Typ, zur Laufzeit festgelegt) |
## Häufig genutzte Kombis (Praxis)
- `as` → Liste von Strings
- `ao` → Liste von Object Paths
- `a{sv}` → „Properties“-Map (String → Variant), sehr verbreitet bei
`org.freedesktop.DBus.Properties`
- `a{ss}` → String → String Map

100
examples/revpi-config Executable file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
from argparse import ArgumentParser, SUPPRESS
from typing import NamedTuple
from pydbus import SessionBus, SystemBus
FeatureMapping = NamedTuple("FeatureMapping", [("dbus_interface", str), ("name", str)])
REVPI_DBUS_NAME = "com.revolutionpi.middleware1"
REVPI_DBUS_BASE_PATH = "/com/revolutionpi/middleware1"
IFACE_SOFTWARE_SERVICES = "com.revolutionpi.middleware1.SoftwareServices"
IFACE_REVPI_CONFIG = "com.revolutionpi.middleware1.RevpiConfig"
REVPI_FEATURE_MAPPINGS = {
"gui": FeatureMapping(IFACE_REVPI_CONFIG, "gui"),
"revpi-con-can": FeatureMapping(IFACE_REVPI_CONFIG, "revpi-con-can"),
"dphys-swapfile": FeatureMapping(IFACE_REVPI_CONFIG, "swapfile"),
"pimodbus-master": FeatureMapping(IFACE_SOFTWARE_SERVICES, "pimodbus-master"),
"pimodbus-slave": FeatureMapping(IFACE_SOFTWARE_SERVICES, "pimodbus-slave"),
"systemd-timesyncd": FeatureMapping(IFACE_SOFTWARE_SERVICES, "ntp"),
"ssh": FeatureMapping(IFACE_SOFTWARE_SERVICES, "ssh"),
"nodered": FeatureMapping(IFACE_SOFTWARE_SERVICES, "nodered"),
"noderedrevpinodes-server": FeatureMapping(IFACE_SOFTWARE_SERVICES, "noderedrevpinodes-server"),
"revpipyload": FeatureMapping(IFACE_SOFTWARE_SERVICES, "revpipyload"),
"bluetooth": FeatureMapping(IFACE_REVPI_CONFIG, "bluetooth"),
"ieee80211": FeatureMapping(IFACE_REVPI_CONFIG, "wlan"),
"avahi": FeatureMapping(IFACE_SOFTWARE_SERVICES, "avahi"),
"external-antenna": FeatureMapping(IFACE_REVPI_CONFIG, "external-antenna"),
}
# Generate command arguments
parser = ArgumentParser(
prog="revpi-config",
description="Configuration tool for Revolution Pi.",
)
parser.add_argument(
"--use-session-bus",
dest="use_session_bus",
action="store_true",
default=False,
help=SUPPRESS,
)
parser.add_argument(
"action",
choices=["enable", "disable", "status", "available", "availstat"],
help="Action to be executed: enable, disable, status or available.",
)
parser.add_argument(
"feature",
nargs="*",
help="Name of the feature to configure.",
)
args = parser.parse_args()
# Init dbus
bus = SessionBus() if args.use_session_bus else SystemBus()
revpi_middleware = bus.get(REVPI_DBUS_NAME, REVPI_DBUS_BASE_PATH)
lst_results = []
for feature in args.feature:
# Get the mappings
feature_mapping = REVPI_FEATURE_MAPPINGS.get(feature, None)
if feature_mapping is None:
if args.action in ("enable", "disable"):
# Missing feature with action enable/disable will return 5
lst_results.append(5)
elif args.action == "availstat":
# Missing feature with action availstat will return 2
lst_results.append(2)
else:
# Missing feature with action status/available will return 0
lst_results.append(0)
continue
dbus_interface = revpi_middleware[feature_mapping.dbus_interface]
if args.action == "enable":
dbus_interface.Enable(feature_mapping.name)
lst_results.append(0)
elif args.action == "disable":
dbus_interface.Disable(feature_mapping.name)
lst_results.append(0)
elif args.action in ("status", "availstat"):
status = dbus_interface.GetStatus(feature_mapping.name)
lst_results.append(int(status))
elif args.action == "available":
availability = dbus_interface.GetAvailability(feature_mapping.name)
lst_results.append(int(availability))
if lst_results:
print(" ".join(map(str, lst_results)))

View File

@@ -1,2 +1,6 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH <support@kunbus.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
[tool.black] [tool.black]
line-length = 100 line-length = 100

View File

@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH <support@kunbus.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
# Build dependencies # Build dependencies
pip-licenses pip-licenses
Pyinstaller Pyinstaller

View File

@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""Metadata of package.""" """Metadata of package."""
__author__ = "Sven Sager" __author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2025 KUNBUS GmbH" __copyright__ = "Copyright (C) 2025 KUNBUS GmbH"
__license__ = " GPL-2.0-or-later" __license__ = " GPL-2.0-or-later"

View File

@@ -2,4 +2,5 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""Package: revpi_middleware.""" """Package: revpi_middleware."""
from .__about__ import __author__, __copyright__, __license__, __version__ from .__about__ import __author__, __copyright__, __license__, __version__

View File

@@ -2,4 +2,5 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""CLI commands to control revpi_middleware.""" """CLI commands to control revpi_middleware."""
from ..__about__ import __author__, __copyright__, __license__, __version__ from ..__about__ import __author__, __copyright__, __license__, __version__

View File

@@ -5,6 +5,7 @@
This module provides the foundation for the RevPi middleware CLI commands This module provides the foundation for the RevPi middleware CLI commands
and argument parsing setup. and argument parsing setup.
""" """
from logging import getLogger from logging import getLogger
from . import cli_config, cli_picontrol from . import cli_config, cli_picontrol

View File

@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""Command-Line for the picontrol object of CLI.""" """Command-Line for the picontrol object of CLI."""
from argparse import ArgumentParser from argparse import ArgumentParser
from logging import getLogger from logging import getLogger
@@ -16,7 +17,7 @@ def add_subparsers(parent_parser: ArgumentParser):
"action", "action",
choices=["enable", "disable", "status", "available", "list-features"], choices=["enable", "disable", "status", "available", "list-features"],
help="Action to be executed: enable, disable, status or available. " help="Action to be executed: enable, disable, status or available. "
"To get all available features, use 'list-features'.", "To get all available features, use 'list-features'.",
) )
parent_parser.add_argument( parent_parser.add_argument(
"feature", "feature",

View File

@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""Command-Line for the picontrol object of CLI.""" """Command-Line for the picontrol object of CLI."""
from argparse import ArgumentParser from argparse import ArgumentParser
from logging import getLogger from logging import getLogger

View File

@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus helper functions for cli commands.""" """D-Bus helper functions for cli commands."""
from enum import Enum from enum import Enum
from threading import Thread from threading import Thread
from time import sleep from time import sleep

View File

@@ -4,12 +4,16 @@
"""Main daemon of revpi-middleware.""" """Main daemon of revpi-middleware."""
from logging import getLogger from logging import getLogger
from os import getuid, environ
from threading import Event from threading import Event
from time import perf_counter from time import perf_counter
from typing import List
from gi.repository import Gio
from . import proginit as pi from . import proginit as pi
from .dbus_middleware1.bus_provider import BusProvider from .dbus_ios1 import BusProviderIos1
from .ios1.bus_provider_io import BusProviderIo from .dbus_middleware1 import BusProviderMiddleware1
log = getLogger(__name__) log = getLogger(__name__)
@@ -26,8 +30,8 @@ class MiddlewareDaemon:
self._reconfigure = False self._reconfigure = False
self._running = True self._running = True
self.bus_provider = None self.bp_middleware1 = None
self.io_bus_provider = None self.bp_ios1 = None
self._configure() self._configure()
log.debug("leave MiddlewareDaemon.__init__") log.debug("leave MiddlewareDaemon.__init__")
@@ -39,35 +43,55 @@ class MiddlewareDaemon:
log.debug("leave MiddlewareDaemon._configure") log.debug("leave MiddlewareDaemon._configure")
@staticmethod
def _get_bus_address() -> str:
if pi.pargs.use_session_bus:
environ_name = "DBUS_SESSION_BUS_ADDRESS"
bus_address = Gio.dbus_address_get_for_bus_sync(Gio.BusType.SESSION)
else:
environ_name = "DBUS_SYSTEM_BUS_ADDRESS"
bus_address = Gio.dbus_address_get_for_bus_sync(Gio.BusType.SYSTEM)
return environ.get(environ_name, bus_address)
def dbus_start(self): def dbus_start(self):
log.debug("enter MiddlewareDaemon.dbus_start") log.debug("enter MiddlewareDaemon.dbus_start")
dbus_middleware_running = self.bus_provider and self.bus_provider.is_alive() dbus_middleware1_running = self.bp_middleware1 and self.bp_middleware1.is_alive()
if not dbus_middleware_running: if not dbus_middleware1_running:
self.bus_provider = BusProvider(use_system_bus=not pi.pargs.use_session_bus) self.bp_middleware1 = BusProviderMiddleware1(self._get_bus_address())
self.bus_provider.start() self.bp_middleware1.start()
dbus_io_middleware_running = self.io_bus_provider and self.io_bus_provider.is_alive() dbus_ios1_running = self.bp_ios1 and self.bp_ios1.is_alive()
if not dbus_io_middleware_running: if not dbus_ios1_running:
self.io_bus_provider = BusProviderIo(use_system_bus=not pi.pargs.use_session_bus) if self.bp_middleware1.published.wait(timeout=10.0):
self.io_bus_provider.start() self.bp_ios1 = BusProviderIos1(self._get_bus_address())
self.bp_ios1.start()
else:
log.error("dbus middleware1 provider thread is not alive - can not start ios1 bus")
log.debug("leave MiddlewareDaemon.dbus_start") log.debug("leave MiddlewareDaemon.dbus_start")
def dbus_stop(self): def dbus_stop(self, bus_filter: List[object] = None):
log.debug("enter MiddlewareDaemon.dbus_stop") log.debug("enter MiddlewareDaemon.dbus_stop")
if self.bus_provider: if bus_filter is None:
self.bus_provider.stop() bus_filter = [
self.bus_provider.join(timeout=10.0) self.bp_middleware1,
if self.bus_provider.is_alive(): self.bp_ios1,
log.warning("dbus provider thread is still alive") ]
if self.io_bus_provider: if self.bp_middleware1 and self.bp_middleware1 in bus_filter:
self.io_bus_provider.stop() self.bp_middleware1.stop()
self.io_bus_provider.join(timeout=10.0) self.bp_middleware1.join(timeout=10.0)
if self.io_bus_provider.is_alive(): if self.bp_middleware1.is_alive():
log.warning("dbus io provider thread is still alive") log.warning("dbus middleware1 provider thread is still alive")
if self.bp_ios1 and self.bp_ios1 in bus_filter:
self.bp_ios1.stop()
self.bp_ios1.join(timeout=10.0)
if self.bp_ios1.is_alive():
log.warning("dbus ios1 provider thread is still alive")
log.debug("leave MiddlewareDaemon.dbus_stop") log.debug("leave MiddlewareDaemon.dbus_stop")
@@ -110,6 +134,17 @@ class MiddlewareDaemon:
self._reconfigure = False self._reconfigure = False
pi.startup_complete() pi.startup_complete()
# Monitor bus providers for errors and restart them
restart = False
if not (self.bp_middleware1 and self.bp_middleware1.is_alive()):
log.warning("dbus middleware1 provider thread is not alive - restarting")
restart = True
if not (self.bp_ios1 and self.bp_ios1.is_alive()):
log.warning("dbus ios1 provider thread is not alive - restarting")
restart = True
if restart:
self.dbus_start()
# Cycle time calculation # Cycle time calculation
dm = divmod(ot - perf_counter(), self._cycle_time) dm = divmod(ot - perf_counter(), self._cycle_time)
# For float the result is (q, a % b), where q is usually math.floor(a / b) but may be 1 less than that. # For float the result is (q, a % b), where q is usually math.floor(a / b) but may be 1 less than that.

View File

@@ -2,4 +2,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus ios version 1 of revpi_middleware.""" """D-Bus ios version 1 of revpi_middleware."""
from .ios1_helper import REVPI_DBUS_BASE_PATH, REVPI_DBUS_NAME from .ios1_helper import REVPI_DBUS_BASE_PATH, REVPI_DBUS_NAME
from .bus_provider_ios1 import BusProviderIos1

View File

@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus bus provider for revpi_middleware."""
from logging import getLogger
from threading import Thread, Event
import revpimodio2
from gi.repository import GLib
from pydbus import connect
from . import REVPI_DBUS_NAME
from .interface_devices import (
InterfaceDeviceManager,
InterfaceDevice,
)
from .interface_ios import (
InterfaceIoManager,
InterfaceInput,
InterfaceOutput,
)
log = getLogger(__name__)
class BusProviderIos1(Thread):
def __init__(
self,
dbus_address: str,
picontrol_device="/dev/piControl0",
config_rsc="/etc/revpi/config.rsc",
):
log.debug("enter BusProviderIos1.__init__")
super().__init__()
self._bus_address = dbus_address
self._loop = GLib.MainLoop()
self._lst_device_interfaces = []
self._lst_io_interfaces = []
self._modio = revpimodio2.RevPiModIO(
procimg=picontrol_device,
configrsc=config_rsc,
shared_procimg=True,
)
self.picontrol_device = picontrol_device
self.published = Event()
self.config_rsc = config_rsc
def run(self):
log.debug("enter BusProviderIos1.run")
bus = connect(self._bus_address)
self._lst_device_interfaces.clear()
self._lst_io_interfaces.clear()
for device in self._modio.device:
self._lst_device_interfaces.append(
InterfaceDevice(bus, device),
)
for io in self._modio.io:
interface = None
try:
if io.type == revpimodio2.INP:
interface = InterfaceInput(bus, io)
elif io.type == revpimodio2.OUT:
interface = InterfaceOutput(bus, io)
elif io.type == revpimodio2.MEM:
# todo: Implement memory
pass
except Exception as e:
log.warning(f"can not create dbus interface for {io.name}: {e}")
if interface is not None:
self._lst_io_interfaces.append(interface)
lst_interfaces = []
lst_interfaces += [
(interface.object_path, interface) for interface in self._lst_device_interfaces
]
lst_interfaces += [
(interface.object_path, interface) for interface in self._lst_io_interfaces
]
try:
bus.publish(
REVPI_DBUS_NAME,
InterfaceDeviceManager(self._lst_device_interfaces),
InterfaceIoManager(self._lst_io_interfaces, self._modio),
*lst_interfaces,
)
self.published.set()
except Exception as e:
log.error(f"can not publish dbus {REVPI_DBUS_NAME}: {e}")
# Subscribe to NotifyDriverReset on middleware1 dbus
iface_pi_control = bus.get(
"com.revolutionpi.middleware1",
"/com/revolutionpi/middleware1",
)["com.revolutionpi.middleware1.PiControl"]
iface_pi_control.onNotifyDriverReset = self.stop
try:
self._loop.run()
except Exception as e:
log.error(f"can not run dbus mainloop: {e}")
bus.con.close()
self._modio.cleanup()
log.debug("leave BusProviderIos1.run")
def stop(self):
log.debug("enter BusProviderIos1.stop")
self._loop.quit()
log.debug("leave BusProviderIos1.stop")
@property
def running(self):
return self._loop.is_running()

View File

@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for IOs."""
from pydbus.bus import Bus
from pydbus.generic import signal
from revpimodio2.device import Device
from .ios1_helper import get_io_object_path, get_device_object_path
class InterfaceDevice:
"""
<node>
<interface name="com.revolutionpi.ios1.Device">
<method name="GetDeviceInputs">
<arg name="object-path-list" type="ao" direction="out"/>
</method>
<method name="GetDeviceOutputs">
<arg name="object-path-list" type="ao" direction="out"/>
</method>
<property name="bmk" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="catalognr" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="comment" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="id" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="name" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="position" type="n" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="type" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.Device"
PropertiesChanged = signal()
def __init__(self, dbus: Bus, device: Device):
self.dbus = dbus
self.device = device
self.object_path = get_device_object_path(device)
def GetDeviceInputs(self) -> list[str]:
return [get_io_object_path(io) for io in self.device.get_inputs()]
def GetDeviceOutputs(self) -> list[str]:
return [get_io_object_path(io) for io in self.device.get_outputs()]
@property
def bmk(self) -> str:
return self.device.bmk
@property
def catalognr(self):
return self.device.catalognr
@property
def comment(self):
return self.device.comment
@property
def id(self):
return self.device.id
@property
def name(self) -> str:
return self.device.name
@property
def position(self) -> int:
return self.device.position
@property
def type(self):
return self.device.type
class InterfaceDeviceManager:
"""
<node>
<interface name="com.revolutionpi.ios1.DeviceManager">
<method name="GetAllDevices">
<arg type="ao" direction="out"/>
</method>
<method name="GetByName">
<arg name="device-name" type="s" direction="in"/>
<arg name="object-path" type="o" direction="out"/>
</method>
<method name="GetByPosition">
<arg name="device-position" type="n" direction="in"/>
<arg name="object-path" type="o" direction="out"/>
</method>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.DeviceManager"
def __init__(
self,
device_interfaces: list[InterfaceDevice],
):
self._lst_device_interfaces = device_interfaces
def GetAllDevices(self) -> list[str]:
return [interface.object_path for interface in self._lst_device_interfaces]
def GetByName(self, device_name) -> str:
for interface in self._lst_device_interfaces:
if interface.device.name == device_name:
return interface.object_path
raise KeyError(f"No device with name '{device_name}' found.")
def GetByPosition(self, device_position) -> str:
for interface in self._lst_device_interfaces:
if interface.device.position == device_position:
return interface.object_path
raise KeyError(f"No device on position '{device_position}' found.")

View File

@@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for IOs."""
from typing import Union, List
from pydbus import Variant
from pydbus.bus import Bus
from pydbus.generic import signal
from revpimodio2 import RevPiModIO, Cycletools, INP, OUT
from revpimodio2.io import IOBase
from .ios1_helper import get_io_object_path, get_variant_type
class InterfaceInput:
"""
<node>
<interface name="com.revolutionpi.ios1.Input">
<method name="SetByteorder">
<arg name="order" type="s" direction="in"/>
</method>
<method name="SetSigned">
<arg name="signed" type="b" direction="in"/>
</method>
<property name="address" type="n" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="bmk" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="bitaddress" type="n" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="byteorder" type="s" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"/>
</property>
<property name="defaultvalue" type="v" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="length" type="q" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="name" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="signed" type="b" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"/>
</property>
<property name="value" type="v" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.Input"
PropertiesChanged = signal()
def __init__(self, dbus: Bus, io: IOBase):
self._raw = False
self.dbus = dbus
self.io = io
self.object_path = get_io_object_path(io)
try:
self.variant_type = get_variant_type(self.io)
except ValueError:
# Fallback to bytes if the integer is too large
self._raw = True
self.variant_type = "ay"
def emit_io_change(self):
self.PropertiesChanged(
self.interface_name,
{
"value": Variant(
self.variant_type,
self.io.get_value() if self._raw else self.io.value,
),
},
[],
)
def SetByteorder(self, order: str) -> None:
self.byteorder = order
def SetSigned(self, signed: bool) -> None:
self.signed = signed
@property
def address(self) -> int:
return self.io.address
@property
def bmk(self) -> str:
return self.io.bmk
@property
def bitaddress(self) -> int:
return self.io._bitaddress
@property
def byteorder(self) -> str:
return self.io.byteorder
@byteorder.setter
def byteorder(self, value: str) -> None:
if hasattr(self.io, "_set_byteorder"):
self.io._set_byteorder(value)
self.variant_type = get_variant_type(self.io)
# Changing the byteorder can change the value, but we do NOT send a signal for that
# because the real value of the process image was not changed. But we inform the client
# about the changed byteorder property.
self.PropertiesChanged(
self.interface_name,
{},
["byteorder"],
)
@property
def defaultvalue(self) -> Variant:
return Variant(
self.variant_type,
self.io.get_value() if self._raw else self.io.defaultvalue,
)
@property
def length(self) -> int:
# 0 length for boolean
return 0 if self.variant_type == "b" else self.io.length
@property
def name(self) -> str:
return self.io.name
@property
def signed(self) -> bool:
if hasattr(self.io, "signed"):
return self.io.signed
return False
@signed.setter
def signed(self, value: bool) -> None:
if hasattr(self.io, "_set_signed"):
self.io._set_signed(value)
self.variant_type = get_variant_type(self.io)
# Changing the signedness can change the value, but we do NOT send a signal for that
# because the real value of the process image was not changed. But we inform the client
# about the changed signedness property.
self.PropertiesChanged(
self.interface_name,
{},
["signed"],
)
@property
def value(self) -> Variant:
if not self.io._parentdevice._selfupdate:
self.io._parentdevice.readprocimg()
return Variant(
self.variant_type,
self.io.get_value() if self._raw else self.io.value,
)
class InterfaceOutput(InterfaceInput):
"""
<node>
<interface name="com.revolutionpi.ios1.Output">
<method name="SetByteorder">
<arg name="order" type="s" direction="in"/>
</method>
<method name="SetSigned">
<arg name="signed" type="b" direction="in"/>
</method>
<method name="SetValue">
<arg name="value" type="v" direction="in"/>
</method>
<property name="address" type="n" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="bmk" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="bitaddress" type="n" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="byteorder" type="s" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"/>
</property>
<property name="defaultvalue" type="v" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="length" type="q" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="name" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="signed" type="b" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"/>
</property>
<property name="value" type="v" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.Output"
def SetValue(self, value: Variant) -> None:
self.value = value
@property
def value(self) -> Variant:
return super().value
@value.setter
def value(self, value: Variant) -> None:
if self._raw:
self.io.set_value(value)
else:
self.io.value = value
if not self.io._parentdevice._selfupdate:
self.io._parentdevice.writeprocimg()
class InterfaceIoManager:
"""
<node>
<interface name="com.revolutionpi.ios1.IoManager">
<method name="GetAllInputs">
<arg name="object-path-list" type="ao" direction="out"/>
</method>
<method name="GetAllOutputs">
<arg name="object-path-list" type="ao" direction="out"/>
</method>
<method name="GetByName">
<arg name="name" type="s" direction="in"/>
<arg name="object-path" type="o" direction="out"/>
</method>
<method name="ActivateIoSignals">
</method>
<method name="DeactivateIoSignals">
</method>
<signal name="IoChanged">
<arg name="name" type="s" direction="out"/>
<arg name="value" type="v" direction="out"/>
</signal>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.IoManager"
IoChanged = signal()
def __init__(
self,
io_interfaces: List[Union[InterfaceInput, InterfaceOutput]],
modio: RevPiModIO,
):
self._dc_io_interfaces = {interface.name: interface for interface in io_interfaces}
self.modio = modio
self.lst_inp_object_path = []
self.lst_out_object_path = []
for interface in io_interfaces:
if interface.io.type == INP:
self.lst_inp_object_path.append(interface.object_path)
elif interface.io.type == OUT:
self.lst_out_object_path.append(interface.object_path)
def _modio_cycle(self, ct: Cycletools) -> None:
for io_name in self._dc_io_interfaces:
interface = self._dc_io_interfaces[io_name]
if ct.changed(interface.io):
interface.emit_io_change()
self.IoChanged(
interface.io.name,
Variant(interface.variant_type, interface.io.value),
)
def GetAllInputs(self) -> list[str]:
return self.lst_inp_object_path
def GetAllOutputs(self) -> list[str]:
return self.lst_out_object_path
def GetByName(self, io_name) -> str:
if io_name in self._dc_io_interfaces:
return self._dc_io_interfaces[io_name].object_path
raise KeyError(f"No IO with name '{io_name}' found.")
def ActivateIoSignals(self) -> None:
if not self.modio._looprunning:
self.modio.autorefresh_all()
self.modio.cycleloop(self._modio_cycle, cycletime=50, blocking=False)
def DeactivateIoSignals(self) -> None:
self.modio.exit(False)

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""Helper for io read and write."""
from logging import getLogger
from revpimodio2.device import Device
from revpimodio2.io import IOBase
log = getLogger(__name__)
REVPI_DBUS_NAME = "com.revolutionpi.ios1"
REVPI_DBUS_BASE_PATH = "/com/revolutionpi/ios1"
def get_io_object_path(io: IOBase) -> str:
return f"{REVPI_DBUS_BASE_PATH}/io/{io.name}"
def get_device_object_path(device: Device) -> str:
return f"{REVPI_DBUS_BASE_PATH}/device/{device.position}"
def get_variant_type(io: IOBase) -> str:
value_type = type(io.value)
byte_length = io.length
signed = io._signed
if value_type is bool:
return "b"
if value_type is float:
return "d"
if value_type is int:
if byte_length <= 2:
return "n" if signed else "q"
if byte_length <= 4:
return "i" if signed else "u"
if byte_length <= 8:
return "x" if signed else "t"
raise ValueError(f"Unsupported byte length: {byte_length}")
if value_type is str:
return "s"
if value_type is bytes:
return "ay"
raise TypeError(f"Unsupported type: {value_type}")

View File

@@ -2,7 +2,8 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus middleware version 1 of revpi_middleware.""" """D-Bus middleware version 1 of revpi_middleware."""
from .dbus_helper import REVPI_DBUS_BASE_PATH, REVPI_DBUS_NAME from .dbus_helper import REVPI_DBUS_BASE_PATH, REVPI_DBUS_NAME
from .dbus_helper import extend_interface from .dbus_helper import extend_interface
from .bus_provider import BusProvider from .bus_provider_middleware1 import BusProviderMiddleware1

View File

@@ -2,38 +2,41 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus bus provider for revpi_middleware.""" """D-Bus bus provider for revpi_middleware."""
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread, Event
from gi.repository import GLib from gi.repository import GLib
from pydbus import SessionBus, SystemBus from pydbus import connect
from . import REVPI_DBUS_NAME from . import REVPI_DBUS_NAME
from .process_image import InterfacePiControl from .process_image import InterfacePiControl
from .system_config import InterfaceRevpiConfig from .system_config import InterfaceRevpiConfig, InterfaceSoftwareServices
log = getLogger(__name__) log = getLogger(__name__)
class BusProvider(Thread): class BusProviderMiddleware1(Thread):
def __init__( def __init__(
self, self,
dbus_address: str,
picontrol_device="/dev/piControl0", picontrol_device="/dev/piControl0",
config_rsc="/etc/revpi/config.rsc", config_rsc="/etc/revpi/config.rsc",
use_system_bus=True,
): ):
log.debug("enter BusProvider.__init__") log.debug("enter BusProviderMiddleware1.__init__")
super().__init__() super().__init__()
self._bus = SystemBus() if use_system_bus else SessionBus() self._bus_address = dbus_address
self._loop = GLib.MainLoop() self._loop = GLib.MainLoop()
self.picontrol_device = picontrol_device self.picontrol_device = picontrol_device
self.published = Event()
self.config_rsc = config_rsc self.config_rsc = config_rsc
def run(self): def run(self):
log.debug("enter BusProvider.run") log.debug("enter BusProviderMiddleware1.run")
bus = connect(self._bus_address)
# The 2nd, 3rd, ... arguments can be objects or tuples of a path and an object # The 2nd, 3rd, ... arguments can be objects or tuples of a path and an object
# Example(), # Example(),
@@ -41,15 +44,17 @@ class BusProvider(Thread):
# ("Subdir2", Example()), # ("Subdir2", Example()),
# ("Subdir2/Whatever", Example()) # ("Subdir2/Whatever", Example())
lst_interfaces = [ lst_interfaces = [
InterfacePiControl(self.picontrol_device, self.config_rsc), InterfacePiControl(bus, self.picontrol_device, self.config_rsc),
InterfaceRevpiConfig(), InterfaceRevpiConfig(bus),
InterfaceSoftwareServices(bus),
] ]
try: try:
self._bus.publish( bus.publish(
REVPI_DBUS_NAME, REVPI_DBUS_NAME,
*lst_interfaces, *lst_interfaces,
) )
self.published.set()
except Exception as e: except Exception as e:
log.error(f"can not publish dbus {REVPI_DBUS_NAME}: {e}") log.error(f"can not publish dbus {REVPI_DBUS_NAME}: {e}")
@@ -58,18 +63,20 @@ class BusProvider(Thread):
except Exception as e: except Exception as e:
log.error(f"can not run dbus mainloop: {e}") log.error(f"can not run dbus mainloop: {e}")
bus.con.close()
# Clean up all interfaces # Clean up all interfaces
for interface in lst_interfaces: for interface in lst_interfaces:
if type(interface) is tuple: if type(interface) is tuple:
_, interface = interface _, interface = interface
interface.cleanup() interface.cleanup()
log.debug("leave BusProvider.run") log.debug("leave BusProviderMiddleware1.run")
def stop(self): def stop(self):
log.debug("enter BusProvider.stop") log.debug("enter BusProviderMiddleware1.stop")
self._loop.quit() self._loop.quit()
log.debug("leave BusProvider.stop") log.debug("leave BusProviderMiddleware1.stop")
@property @property
def running(self): def running(self):

View File

@@ -5,6 +5,8 @@
from logging import getLogger from logging import getLogger
from pydbus.bus import Bus
log = getLogger(__name__) log = getLogger(__name__)
REVPI_DBUS_NAME = "com.revolutionpi.middleware1" REVPI_DBUS_NAME = "com.revolutionpi.middleware1"
@@ -13,6 +15,9 @@ REVPI_DBUS_BASE_PATH = "/com/revolutionpi/middleware1"
class DbusInterface: class DbusInterface:
def __init__(self, bus: Bus):
self.bus = bus
def cleanup(self): def cleanup(self):
""" """
Represents a method responsible for performing cleanup operations. This method is executed to properly Represents a method responsible for performing cleanup operations. This method is executed to properly

View File

@@ -2,4 +2,5 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for piControl driver.""" """D-Bus interfaces for piControl driver."""
from .interface_picontrol import InterfacePiControl from .interface_picontrol import InterfacePiControl

View File

@@ -2,8 +2,10 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for piControl.""" """D-Bus interfaces for piControl."""
from logging import getLogger from logging import getLogger
from pydbus.bus import Bus
from pydbus.generic import signal from pydbus.generic import signal
from .process_image_helper import PiControlIoctl, ResetDriverWatchdog from .process_image_helper import PiControlIoctl, ResetDriverWatchdog
@@ -26,7 +28,9 @@ class InterfacePiControl(DbusInterface):
NotifyDriverReset = signal() NotifyDriverReset = signal()
def __init__(self, picontrol_device: str, config_rsc: str): def __init__(self, bus: Bus, picontrol_device: str, config_rsc: str):
super().__init__(bus)
self.picontrol_device = picontrol_device self.picontrol_device = picontrol_device
self.config_rsc = config_rsc self.config_rsc = config_rsc

View File

@@ -7,6 +7,7 @@ Helper for the process image.
The ResetDriverWatchdog class is a copy of revpipyload project module "watchdogs" The ResetDriverWatchdog class is a copy of revpipyload project module "watchdogs"
https://github.com/naruxde/revpipyload/blob/b51c2b617a57cc7d96fd67e1da9f090a0624eacb/src/revpipyload/watchdogs.py https://github.com/naruxde/revpipyload/blob/b51c2b617a57cc7d96fd67e1da9f090a0624eacb/src/revpipyload/watchdogs.py
""" """
import os import os
from ctypes import c_int from ctypes import c_int
from fcntl import ioctl from fcntl import ioctl

View File

@@ -2,4 +2,6 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for system configuration.""" """D-Bus interfaces for system configuration."""
from .interface_config import InterfaceRevpiConfig from .interface_config import InterfaceRevpiConfig
from .interface_services import InterfaceSoftwareServices

View File

@@ -2,19 +2,20 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for hardware configuration.""" """D-Bus interfaces for hardware configuration."""
from collections import namedtuple from collections import namedtuple
from logging import getLogger from logging import getLogger
from pydbus.generic import signal
from .revpi_config import ( from .revpi_config import (
ConfigActions, ConfigActions,
configure_avahi_daemon,
configure_bluetooth, configure_bluetooth,
configure_con_can, configure_con_can,
configure_dphys_swapfile, configure_dphys_swapfile,
configure_external_antenna, configure_external_antenna,
configure_gui, configure_gui,
configure_wlan, configure_wlan,
simple_systemd,
) )
from ..dbus_helper import DbusInterface from ..dbus_helper import DbusInterface
@@ -42,19 +43,32 @@ class InterfaceRevpiConfig(DbusInterface):
<arg name="available" type="b" direction="out"/> <arg name="available" type="b" direction="out"/>
</method> </method>
<property name="available_features" type="as" access="read"/> <property name="available_features" type="as" access="read"/>
<signal name="StatusChanged">
<arg name="feature" type="s"/>
<arg name="status" type="b"/>
</signal>
<signal name="AvailabilityChanged">
<arg name="feature" type="s"/>
<arg name="available" type="b"/>
</signal>
</interface> </interface>
</node> </node>
""" """
AvailabilityChanged = signal()
StatusChanged = signal()
def Disable(self, feature: str) -> None: def Disable(self, feature: str) -> None:
"""Disable the feature.""" """Disable the feature."""
feature_function = get_feature(feature) feature_function = get_feature(feature)
feature_function.function(ConfigActions.DISABLE, *feature_function.args) feature_function.function(ConfigActions.DISABLE, *feature_function.args)
self.StatusChanged(feature, False)
def Enable(self, feature: str) -> None: def Enable(self, feature: str) -> None:
"""Enable the feature.""" """Enable the feature."""
feature_function = get_feature(feature) feature_function = get_feature(feature)
feature_function.function(ConfigActions.ENABLE, *feature_function.args) feature_function.function(ConfigActions.ENABLE, *feature_function.args)
self.StatusChanged(feature, True)
def GetStatus(self, feature: str) -> bool: def GetStatus(self, feature: str) -> bool:
"""Get feature status.""" """Get feature status."""
@@ -83,18 +97,8 @@ def get_feature(feature: str) -> FeatureFunction:
AVAILABLE_FEATURES = { AVAILABLE_FEATURES = {
"gui": FeatureFunction(configure_gui, []), "gui": FeatureFunction(configure_gui, []),
"revpi-con-can": FeatureFunction(configure_con_can, []), "revpi-con-can": FeatureFunction(configure_con_can, []),
"dphys-swapfile": FeatureFunction(configure_dphys_swapfile, []), "swapfile": FeatureFunction(configure_dphys_swapfile, []),
"pimodbus-master": FeatureFunction(simple_systemd, ["pimodbus-master.service"]),
"pimodbus-slave": FeatureFunction(simple_systemd, ["pimodbus-slave.service"]),
"systemd-timesyncd": FeatureFunction(simple_systemd, ["systemd-timesyncd.service"]),
"ssh": FeatureFunction(simple_systemd, ["ssh.service"]),
"nodered": FeatureFunction(simple_systemd, ["nodered.service"]),
"noderedrevpinodes-server": FeatureFunction(
simple_systemd, ["noderedrevpinodes-server.service"]
),
"revpipyload": FeatureFunction(simple_systemd, ["revpipyload.service"]),
"bluetooth": FeatureFunction(configure_bluetooth, []), "bluetooth": FeatureFunction(configure_bluetooth, []),
"wlan": FeatureFunction(configure_wlan, []), "wlan": FeatureFunction(configure_wlan, []),
"avahi": FeatureFunction(configure_avahi_daemon, []),
"external-antenna": FeatureFunction(configure_external_antenna, []), "external-antenna": FeatureFunction(configure_external_antenna, []),
} }

View File

@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for software services."""
from logging import getLogger
from typing import List
from pydbus.bus import Bus
from pydbus.generic import signal
from ..dbus_helper import DbusInterface
from ..systemd_helper import simple_systemd, ServiceActions
log = getLogger(__name__)
class InterfaceSoftwareServices(DbusInterface):
"""
<node>
<interface name="com.revolutionpi.middleware1.SoftwareServices">
<method name="Disable">
<arg name="feature" type="s" direction="in"/>
</method>
<method name="Enable">
<arg name="feature" type="s" direction="in"/>
</method>
<method name="GetStatus">
<arg name="feature" type="s" direction="in"/>
<arg name="status" type="b" direction="out"/>
</method>
<method name="GetAvailability">
<arg name="feature" type="s" direction="in"/>
<arg name="available" type="b" direction="out"/>
</method>
<property name="available_features" type="as" access="read"/>
<signal name="StatusChanged">
<arg name="feature" type="s"/>
<arg name="status" type="b"/>
</signal>
<signal name="AvailabilityChanged">
<arg name="feature" type="s"/>
<arg name="available" type="b"/>
</signal>
</interface>
</node>
"""
AvailabilityChanged = signal()
StatusChanged = signal()
def __init__(self, bus: Bus):
super().__init__(bus)
self.mrk_available = {}
self.mrk_status = {}
self.services = {
"pimodbus-master": ["pimodbus-master.service"],
"pimodbus-slave": ["pimodbus-slave.service"],
"ntp": ["systemd-timesyncd.service"],
"ssh": ["ssh.service"],
"nodered": ["nodered.service"],
"noderedrevpinodes-server": ["noderedrevpinodes-server.service"],
"revpipyload": ["revpipyload.service"],
"avahi": ["avahi-daemon.service", "avahi-daemon.socket"],
}
# Create a systemd manager interface object
systemd = self.bus.get(
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
)
systemd_manager = systemd["org.freedesktop.systemd1.Manager"]
# Load all unit paths and subscribe to properties changed signal
self.object_paths = {}
for feature in self.services:
# Get the status and availability of the feature
self.mrk_available[feature] = self.GetAvailability(feature)
self.mrk_status[feature] = self.GetStatus(feature)
# Subscribe to properties changed signal for each unit
for unit_name in self.services[feature]:
unit_path = systemd_manager.LoadUnit(unit_name)
self.object_paths[unit_path] = feature
self.bus.subscribe(
iface="org.freedesktop.DBus.Properties",
signal="PropertiesChanged",
object=unit_path,
signal_fired=self._callback_properties_changed,
)
# Subscribe to the reloading signal to update the availability of the feature
self.bus.subscribe(
iface="org.freedesktop.systemd1.Manager",
signal="Reloading",
object="/org/freedesktop/systemd1",
signal_fired=self._callback_reloading_signal,
)
def _callback_reloading_signal(self, sender, object_path, interface, signal, parameters):
"""
Handles the signal emitted for reloading and checks for changes in availability
and status for a set of services. If changes are identified, corresponding
update methods are triggered to reflect the new states.
Args:
sender: The entity sending the signal.
object_path: Path to the object emitting the signal.
interface: Interface through which the signal is sent.
signal: The signal being received.
parameters: A list of parameters associated with the signal.
Raises:
None
"""
if parameters[0]:
return
for feature in self.services:
availability = self.GetAvailability(feature)
if self.mrk_available[feature] != availability:
self.mrk_available[feature] = availability
self.AvailabilityChanged(feature, availability)
status = self.GetStatus(feature)
if self.mrk_status[feature] != status:
self.mrk_status[feature] = status
self.StatusChanged(feature, status)
def _callback_properties_changed(self, sender, object_path, interface, signal, parameters):
"""
Handles the 'PropertiesChanged' signal callback by updating internal status tracking for given
features and invoking status change notifications if necessary.
Args:
sender (Any): Information about the signal sender.
object_path (str): The path of the object emitting the signal.
interface (str): The interface where the signal was emitted.
signal (str): The name of the emitted signal.
parameters (tuple): Signal parameters containing interface name, changed properties, and
invalidated properties.
Raises:
TypeError: If the 'parameters' argument does not unpack to three expected values.
"""
interface, changed_properties, invalidated_properties = parameters
if "ActiveState" not in changed_properties:
return
feature = self.object_paths[object_path]
status = self.GetStatus(feature)
if self.mrk_status[feature] != status:
self.mrk_status[feature] = status
self.StatusChanged(feature, status)
def _get_unit_names(self, feature: str) -> List[str]:
if feature not in self.services:
raise ValueError(f"feature {feature} does not exist")
return self.services[feature]
def Disable(self, feature: str) -> None:
"""Disable the feature."""
action = ServiceActions.DISABLE
unit_names = self._get_unit_names(feature)
for unit_name in unit_names:
simple_systemd(action, unit_name)
def Enable(self, feature: str) -> None:
"""Enable the feature."""
action = ServiceActions.ENABLE
unit_names = self._get_unit_names(feature)
for unit_name in unit_names:
simple_systemd(action, unit_name)
def GetStatus(self, feature: str) -> bool:
"""Get feature status."""
unit_names = self._get_unit_names(feature)
rc_status = True
for unit_name in unit_names:
if not simple_systemd(ServiceActions.STATUS, unit_name):
rc_status = False
break
return rc_status
def GetAvailability(self, feature: str) -> bool:
"""Get feature availability on the RevPi."""
unit_names = self._get_unit_names(feature)
rc_available = True
for unit_name in unit_names:
if not simple_systemd(ServiceActions.AVAILABLE, unit_name):
rc_available = False
break
return rc_available
@property
def available_features(self) -> list[str]:
return list(self.services.keys())

View File

@@ -10,12 +10,12 @@ from glob import glob
from logging import getLogger from logging import getLogger
from os import X_OK, access from os import X_OK, access
from os.path import exists, join from os.path import exists, join
from threading import Thread
from typing import List, Optional from typing import List, Optional
from pydbus import SystemBus from pydbus import SystemBus
from ..dbus_helper import grep from ..dbus_helper import grep
from ..systemd_helper import simple_systemd, ServiceActions
log = getLogger(__name__) log = getLogger(__name__)
@@ -42,6 +42,7 @@ class ComputeModuleTypes(IntEnum):
CM4S (int): Represents a Compute Module 4S. CM4S (int): Represents a Compute Module 4S.
CM5 (int): Represents a Compute Module 5. CM5 (int): Represents a Compute Module 5.
""" """
UNKNOWN = 0 UNKNOWN = 0
CM1 = 6 CM1 = 6
CM3 = 10 CM3 = 10
@@ -58,6 +59,7 @@ class ConfigActions(Enum):
actions. It can be used to ensure consistency when working with or defining actions. It can be used to ensure consistency when working with or defining
such actions in a system. such actions in a system.
""" """
ENABLE = "enable" ENABLE = "enable"
DISABLE = "disable" DISABLE = "disable"
STATUS = "status" STATUS = "status"
@@ -232,6 +234,7 @@ class ConfigTxt:
_config_txt_lines (list[str]): Contains all lines of the configuration _config_txt_lines (list[str]): Contains all lines of the configuration
file as a list of strings, where each string represents a line. file as a list of strings, where each string represents a line.
""" """
re_name_value = re.compile(r"^\s*(?!#)(?P<name>[^=\s].+?)\s*=\s*(?P<value>\S+)\s*$") re_name_value = re.compile(r"^\s*(?!#)(?P<name>[^=\s].+?)\s*=\s*(?P<value>\S+)\s*$")
def __init__(self): def __init__(self):
@@ -432,18 +435,6 @@ class ConfigTxt:
return self._config_txt_path return self._config_txt_path
def configure_avahi_daemon(action: ConfigActions):
return_value = simple_systemd(action, "avahi-daemon.service")
# Post actions for avahi-daemon
if action in (ConfigActions.ENABLE, ConfigActions.DISABLE):
# Apply the enable/disable action to the avahi socket AFTER the service
# unit, because a connected socket could interrupt stop
simple_systemd(action, "avahi-daemon.socket")
return return_value
def configure_bluetooth(action: ConfigActions): def configure_bluetooth(action: ConfigActions):
hci_device = join(LINUX_BT_CLASS_PATH, "hci0") hci_device = join(LINUX_BT_CLASS_PATH, "hci0")
bt_rfkill_index = get_rfkill_index(hci_device) bt_rfkill_index = get_rfkill_index(hci_device)
@@ -509,7 +500,17 @@ def configure_con_can(action: ConfigActions):
def configure_dphys_swapfile(action: ConfigActions): def configure_dphys_swapfile(action: ConfigActions):
return_value = simple_systemd(action, "dphys-swapfile.service") # Translate config action to systemd action
if action is ConfigActions.ENABLE:
systemd_action = ServiceActions.ENABLE
elif action is ConfigActions.DISABLE:
systemd_action = ServiceActions.DISABLE
elif action is ConfigActions.STATUS:
systemd_action = ServiceActions.STATUS
else:
systemd_action = ServiceActions.AVAILABLE
return_value = simple_systemd(systemd_action, "dphys-swapfile.service")
# Post actions for dphys-swapfile # Post actions for dphys-swapfile
if action is ConfigActions.DISABLE: if action is ConfigActions.DISABLE:
@@ -634,98 +635,6 @@ def get_rfkill_index(device_class_path: str) -> Optional[int]:
return None return None
def simple_systemd(action: ConfigActions, unit: str):
"""
Performs specified actions on systemd units.
This function allows interaction with systemd units for various operations
such as enabling, disabling, checking the status, and verifying availability.
It communicates with the systemd manager via the SystemBus and handles units
based on the action specified.
Parameters:
action (ConfigActions): Specifies the action to be performed on the
systemd unit. Supported actions include ENABLE,
DISABLE, STATUS, and AVAILABLE.
unit (str): The name of the systemd unit on which the action is to be
performed.
Returns:
bool: For STATUS and AVAILABLE actions, returns True if the corresponding
criteria are met (e.g., enabled and active for STATUS, or not found
for AVAILABLE). Otherwise, returns False.
Raises:
ValueError: If the specified action is not supported.
"""
bus = SystemBus()
systemd = bus.get(
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
)
systemd_manager = systemd["org.freedesktop.systemd1.Manager"]
if action is ConfigActions.ENABLE:
def thread_unit_config():
"""Change configuration asynchronously."""
# Dbus call: UnmaskUnitFiles(in as files, in b runtime, out a(sss) changes
lst_change_unmask = systemd_manager.UnmaskUnitFiles([unit], False)
# Dbus call: EnableUnitFiles(in as files, in b runtime, in b force,
# out b carries_install_info, out a(sss) changes
lst_change_enable = systemd_manager.EnableUnitFiles([unit], False, False)
if lst_change_unmask or lst_change_enable:
# Reload systemd after modified unit property
systemd_manager.Reload()
Thread(target=thread_unit_config, daemon=True).start()
# Dbus call: StartUnit(in s name, in s mode, out o job
systemd_manager.StartUnit(unit, "replace")
elif action is ConfigActions.DISABLE:
def thread_unit_config():
"""Change configuration asynchronously."""
# Dbus call: DisableUnitFiles (in as files, in b runtime, out a(sss) changes)
change = systemd_manager.DisableUnitFiles([unit], False)
if change:
# Reload systemd after modified unit property
systemd_manager.Reload()
Thread(target=thread_unit_config, daemon=True).start()
# Dbus call: StopUnit(in s name,in s mode, out o job
systemd_manager.StopUnit(unit, "replace")
elif action is ConfigActions.STATUS:
try:
unit_path = systemd_manager.LoadUnit(unit)
properties = bus.get("org.freedesktop.systemd1", unit_path)
except Exception:
log.warning(f"could not get systemd unit {unit}")
return False
return properties.UnitFileState == "enabled" and properties.ActiveState == "active"
elif action is ConfigActions.AVAILABLE:
try:
unit_path = systemd_manager.LoadUnit(unit)
properties = bus.get("org.freedesktop.systemd1", unit_path)
except Exception:
log.warning(f"could not get systemd unit {unit}")
return False
return properties.LoadState != "not-found"
else:
raise ValueError(f"action {action} not supported")
return None
if __name__ == "__main__": if __name__ == "__main__":
rc = RevPiConfig() rc = RevPiConfig()
print("Model:", rc.model) print("Model:", rc.model)

View File

@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
from enum import Enum
from logging import getLogger
from threading import Thread
from typing import Optional
from pydbus import SystemBus
log = getLogger(__name__)
class ServiceActions(Enum):
"""
Enumeration class for defining configuration actions.
This enumeration provides predefined constants for common configuration
actions. It can be used to ensure consistency when working with or defining
such actions in a system.
"""
ENABLE = "enable"
DISABLE = "disable"
STATUS = "status"
AVAILABLE = "available"
def simple_systemd(action: ServiceActions, unit: str, unmask: bool = False) -> Optional[bool]:
"""
Perform systemd service actions such as enable, disable, check status, or availability.
This function interacts with the systemd D-Bus API to manage and query the
state of services on a system. The supported actions include enabling a systemd
unit, disabling it, starting/stopping a unit, and checking its status or
availability. The function supports asynchronous configuration changes through
threads where applicable.
Parameters:
action (ServiceActions): The action to perform on the systemd service.
Supported actions are ENABLE, DISABLE, STATUS, and AVAILABLE.
unit (str): The name of the systemd unit to operate on (e.g., "example.service").
unmask (bool): When enabling a unit, if True, any masked unit file will
first be unmasked before proceeding with the operation. Defaults to False.
Returns:
Optional[bool]: The return value depends on the action. For STATUS or
AVAILABLE actions, it returns True if the unit satisfies the condition
(e.g., enabled and active, or available and loaded), False otherwise.
For other actions, it returns None.
"""
bus = SystemBus()
systemd = bus.get(
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
)
systemd_manager = systemd["org.freedesktop.systemd1.Manager"]
if action is ServiceActions.ENABLE:
def thread_unit_config():
"""Change configuration asynchronously."""
lst_change_unmask = []
if unmask:
# Dbus call: UnmaskUnitFiles(in as files, in b runtime, out a(sss) changes
lst_change_unmask = systemd_manager.UnmaskUnitFiles([unit], False)
# Dbus call: EnableUnitFiles(in as files, in b runtime, in b force,
# out b carries_install_info, out a(sss) changes
lst_change_enable = systemd_manager.EnableUnitFiles([unit], False, False)
if lst_change_unmask or lst_change_enable:
# Reload systemd after modified unit property
systemd_manager.Reload()
Thread(target=thread_unit_config, daemon=True).start()
# Dbus call: StartUnit(in s name, in s mode, out o job
systemd_manager.StartUnit(unit, "replace")
elif action is ServiceActions.DISABLE:
def thread_unit_config():
"""Change configuration asynchronously."""
# Dbus call: DisableUnitFiles (in as files, in b runtime, out a(sss) changes)
change = systemd_manager.DisableUnitFiles([unit], False)
if change:
# Reload systemd after modified unit property
systemd_manager.Reload()
Thread(target=thread_unit_config, daemon=True).start()
# Dbus call: StopUnit(in s name,in s mode, out o job
systemd_manager.StopUnit(unit, "replace")
elif action is ServiceActions.STATUS:
try:
unit_path = systemd_manager.LoadUnit(unit)
properties = bus.get("org.freedesktop.systemd1", unit_path)
except Exception:
log.warning(f"could not get systemd unit {unit}")
return False
return properties.UnitFileState == "enabled" and properties.ActiveState in (
"active",
"activating",
)
elif action is ServiceActions.AVAILABLE:
try:
unit_path = systemd_manager.LoadUnit(unit)
properties = bus.get("org.freedesktop.systemd1", unit_path)
except Exception:
log.warning(f"could not get systemd unit {unit}")
return False
return properties.LoadState != "not-found"
else:
raise ValueError(f"action {action} not supported")
return None

View File

@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus bus provider for revpi_middleware."""
from logging import getLogger
from threading import Thread
import revpimodio2
from gi.repository import GLib
from pydbus import SessionBus, SystemBus
from revpimodio2 import Cycletools
from . import REVPI_DBUS_NAME
from .interface_ios import (
InterfaceIoManager,
InterfaceInpBool,
InterfaceOutBool,
InterfaceInpInt,
InterfaceOutInt,
)
log = getLogger(__name__)
class BusProviderIo(Thread):
def __init__(
self,
picontrol_device="/dev/piControl0",
config_rsc="/etc/revpi/config.rsc",
use_system_bus=True,
):
log.debug("enter BusProviderIo.__init__")
super().__init__()
self._bus = SystemBus() if use_system_bus else SessionBus()
self._dc_io_interfaces = {}
self._loop = GLib.MainLoop()
self._modio = revpimodio2.RevPiModIO(
procimg=picontrol_device,
configrsc=config_rsc,
shared_procimg=True,
)
self.picontrol_device = picontrol_device
self.config_rsc = config_rsc
def run(self):
log.debug("enter BusProviderIo.run")
self._dc_io_interfaces.clear()
for io in self._modio.io:
interface = None
value_type = type(io.value)
if value_type is bool:
interface = (
InterfaceInpBool(self._bus, io)
if io.type == revpimodio2.INP
else InterfaceOutBool(self._bus, io)
)
elif value_type is int:
interface = (
InterfaceInpInt(self._bus, io)
if io.type == revpimodio2.INP
else InterfaceOutInt(self._bus, io)
)
if interface is not None:
self._dc_io_interfaces[io.name] = interface
lst_interfaces = [
(f"io/{io_name}", self._dc_io_interfaces[io_name]) for io_name in self._dc_io_interfaces
]
try:
self._bus.publish(
REVPI_DBUS_NAME,
InterfaceIoManager(self._modio, self._dc_io_interfaces),
*lst_interfaces,
)
except Exception as e:
log.error(f"can not publish dbus {REVPI_DBUS_NAME}: {e}")
try:
self._loop.run()
except Exception as e:
log.error(f"can not run dbus mainloop: {e}")
self._modio.cleanup()
log.debug("leave BusProviderIo.run")
def stop(self):
log.debug("enter BusProviderIo.stop")
self._loop.quit()
log.debug("leave BusProviderIo.stop")
@property
def running(self):
return self._loop.is_running()

View File

@@ -1,159 +0,0 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for IOs."""
from gi.overrides.GLib import Variant
from pydbus.generic import signal
from revpimodio2 import RevPiModIO, Cycletools
from .ios1_helper import REVPI_DBUS_BASE_PATH, DbusInterfaceIo
class InterfaceIoManager:
"""
<node>
<interface name="com.revolutionpi.ios1.IoManager">
<method name="GetAllInputs">
<arg type="as" direction="out"/>
</method>
<method name="GetAllOutputs">
<arg type="as" direction="out"/>
</method>
<method name="Get">
<arg name="io_name" type="s" direction="in"/>
<arg name="object-path" type="o" direction="out"/>
</method>
<method name="ActivateIoEvents">
</method>
<method name="DeactivateIoEvents">
</method>
<signal name="IoChanged">
<arg name="name" type="s" direction="out"/>
<arg name="value" type="v" direction="out"/>
</signal>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.IoManager"
IoChanged = signal()
def __init__(self, modio: RevPiModIO, io_interfaces: dict[str, DbusInterfaceIo]):
self._dc_io_interfaces = io_interfaces
self.modio = modio
self.lst_inp = []
for dev in self.modio.device:
for io in dev.get_inputs():
self.lst_inp.append(self._get_io_path(io.name))
self.lst_out = []
for dev in self.modio.device:
for io in dev.get_outputs():
self.lst_out.append(self._get_io_path(io.name))
def _modio_cycle(self, ct: Cycletools) -> None:
for io_name in self._dc_io_interfaces:
interface = self._dc_io_interfaces[io_name]
if ct.changed(interface.io):
interface.emit_io_change()
self.IoChanged(interface.io.name, Variant("b", interface.io.value))
def _get_io_path(self, io_name: str) -> str:
return f"{REVPI_DBUS_BASE_PATH}/io/{io_name}"
def GetAllInputs(self) -> list[str]:
return self.lst_inp
def GetAllOutputs(self) -> list[str]:
return self.lst_out
def Get(self, io_name) -> str:
if io_name in self.modio.io:
return self._get_io_path(io_name)
raise KeyError(f"No IO with name '{io_name}' found.")
def ActivateIoEvents(self) -> None:
if not self.modio._looprunning:
self.modio.autorefresh_all()
self.modio.cycleloop(self._modio_cycle, cycletime=50, blocking=False)
def DeactivateIoEvents(self) -> None:
self.modio.exit(False)
class InterfaceInpBool(DbusInterfaceIo):
"""
<node>
<interface name="com.revolutionpi.ios1.InpBool">
<property name="bmk" type="s" access="read"/>
<property name="name" type="s" access="read"/>
<property name="value" type="b" access="read"/>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.InpBool"
@property
def bmk(self) -> str:
return self.io.bmk
@property
def name(self) -> str:
return self.io.name
@property
def value(self) -> bool:
return self.io.value
class InterfaceOutBool(InterfaceInpBool):
"""
<node>
<interface name="com.revolutionpi.ios1.OutBool">
<property name="bmk" type="s" access="read"/>
<property name="name" type="s" access="read"/>
<property name="value" type="b" access="readwrite"/>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.OutBool"
@property
def value(self) -> bool:
return super().value
@value.setter
def value(self, value: bool):
self.io.value = value
self.io._parentdevice._modio.writeprocimg()
class InterfaceInpInt(DbusInterfaceIo):
"""
<node>
<interface name="com.revolutionpi.ios1.InpInt">
<property name="bmk" type="s" access="read"/>
<property name="name" type="s" access="read"/>
<property name="value" type="i" access="read"/>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.InpInt"
class InterfaceOutInt(InterfaceInpInt):
"""
<node>
<interface name="com.revolutionpi.ios1.OutInt">
<property name="bmk" type="s" access="read"/>
<property name="name" type="s" access="read"/>
<property name="value" type="i" access="readwrite"/>
</interface>
</node>
"""
interface_name = "com.revolutionpi.ios1.OutInt"

View File

@@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""Helper for io read and write."""
from logging import getLogger
from gi.overrides.GLib import Variant
from pydbus import SessionBus, SystemBus
from pydbus.generic import signal
from revpimodio2.io import IOBase
log = getLogger(__name__)
REVPI_DBUS_NAME = "com.revolutionpi.ios1"
REVPI_DBUS_BASE_PATH = "/com/revolutionpi/ios1"
class DbusInterfaceIo:
interface_name = ""
PropertiesChanged = signal()
def __init__(self, dbus: SystemBus or SessionBus, io: IOBase):
self.dbus = dbus
self.io = io
self.variant_type = "b" if type(self.io.value) is bool else "i"
def emit_io_change(self):
if not self.interface_name:
return
if self.interface_name:
print(type(self.io.value))
self.PropertiesChanged(
self.interface_name,
{"value": int(self.io.value)},
[],
)

View File

@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH # SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
"""Main application of revpi-middleware daemon.""" """Main application of revpi-middleware daemon."""
from logging import getLogger from logging import getLogger
from .daemon import MiddlewareDaemon from .daemon import MiddlewareDaemon

View File

@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: 2018-2023 Sven Sager # SPDX-FileCopyrightText: 2018-2023 Sven Sager
# SPDX-License-Identifier: LGPL-2.0-or-later # SPDX-License-Identifier: LGPL-2.0-or-later
"""Global program initialization.""" """Global program initialization."""
__author__ = "Sven Sager" __author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018-2023 Sven Sager" __copyright__ = "Copyright (C) 2018-2023 Sven Sager"
__license__ = "LGPL-2.0-or-later" __license__ = "LGPL-2.0-or-later"

View File

@@ -3,6 +3,8 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
from time import sleep from time import sleep
from gi.repository.Gio import dbus_address_get_for_bus_sync, BusType
from tests.dbus_middleware1.fake_devices import PiControlDeviceMockup from tests.dbus_middleware1.fake_devices import PiControlDeviceMockup
@@ -12,12 +14,13 @@ class TestBusProvider(PiControlDeviceMockup):
super().setUp() super().setUp()
# Do not import things on top of the module. Some classes or functions need to be mocked up first. # Do not import things on top of the module. Some classes or functions need to be mocked up first.
from revpi_middleware.dbus_middleware1 import BusProvider from revpi_middleware.dbus_middleware1 import BusProviderMiddleware1
# Prepare the bus provider and start it # Prepare the bus provider and start it
self.bp = BusProvider( bus_address = dbus_address_get_for_bus_sync(BusType.SESSION)
self.picontrol.name, self.bp = BusProviderMiddleware1(
use_system_bus=False, bus_address,
picontrol_device=self.picontrol.name,
) )
self.bp.start() self.bp.start()