├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── doc └── intro.txt ├── install ├── install.d ├── build │ ├── README │ └── pkg-r ├── fbset │ ├── README │ └── pkg-r ├── flashmq │ ├── README │ ├── pkg-r │ ├── post │ └── pre └── moat │ ├── README │ ├── pkg-r │ ├── post │ └── pre ├── lib └── bin │ ├── gen-flashmq-conf │ ├── get-unique-id │ ├── iptables-redirect │ ├── serial-add │ ├── softlimit │ ├── start-gui │ ├── udev-handler │ ├── vbus │ ├── vctl │ └── ven ├── patch ├── dbus-modbus-client │ ├── client.py │ ├── dbus-modbus-client.py │ └── dbus-modbus-client.py.V │ │ └── v3.54 ├── dbus-mqtt │ ├── dbus_mqtt.py │ └── mqtt_gobject_bridge.py ├── dbus-shelly │ └── dbus_shelly.py ├── dbus-systemcalc-py │ └── sc_utils.py ├── mk2-dbus │ └── start-mk2-dbus.sh └── vedirect-interface │ └── start-vedirect.sh ├── service ├── flashmq.service ├── mk3@.service ├── modbus@.service ├── udev-handler.service └── vedirect@.service ├── udev.example.yml └── udev.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /venus-image-* 2 | ext/*.deb 3 | ext/*.changes 4 | ext/*.build 5 | ext/*.buildinfo 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/dbus-flashmq"] 2 | path = ext/dbus-flashmq 3 | url = https://github.com/M-o-a-T/dbus-flashmq.git 4 | [submodule "ext/flashmq"] 5 | path = ext/flashmq 6 | url = https://github.com/M-o-a-T/flashmq.git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This archive is copyright 🄯 2024 by Matthias Urlichs ‹matthias@urlichs.de› 2 | and/or any other contributors, as shown by "git log". 3 | 4 | This work is licensed under the MIT License, as used by Victron. 5 | 6 | However, the author(s) would like to request that all bugfixes, 7 | enhancements, et al., are made available to them, preferably via 8 | a publicly-available git repository. 9 | 10 | Thank you. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Venusian 2 | 3 | "The Venusian" is a hybrid of Victron Energy's "venus" operating system for the 4 | Raspberry Pi (and some others), and Debian GNU/Linux. 5 | 6 | The Venusian allows you to run a Victron system on any Debian-based system. 7 | This includes your Intel laptop. 8 | 9 | This is of course impossible — but we're doing it anyway. 10 | 11 | ## Rationale 12 | 13 | You might already have a Debian-based server in the same room as your Victron 14 | installation, and yet another computer that nibbles away at your battery 15 | isn't what you signed up for. 16 | 17 | You might want to run code on your Victron system that won't work with 18 | Python 8, or some other software that's not packaged for Victron's 19 | OpenEmbedded installation. (NB, Python 8 is end-of-life in mid-2024.) 20 | 21 | You might need a kernel module that the Victron kernel doesn't support. 22 | 23 | You might want to re-use the backup solution you already have, instead of 24 | coding some custom solution for Venus. 25 | 26 | You might be interested in plugging a USB stick into your Venus 27 | installation for RAID-0. 28 | 29 | You might want to stop using the antiquated daemontools' supervisor and 30 | runtime system because they don't talk to dbus or anything, 31 | indiscriminately restart things which are broken or you don't need, and/or 32 | don't send their logs anywhere. 33 | 34 | … or maybe you just don't particularly like OpenEmbedded. 35 | 36 | 37 | ## Usage 38 | 39 | * Install (see below). 40 | * `systemctl start user@venus` 41 | * The Venus GUI is available on VNC port 5901. 42 | 43 | For external network access, you either need an `iptables` rule is required. 44 | 45 | ### MQTT 46 | 47 | The Venus system more-or-less-requires a FlashMQ server, with a plug-in 48 | that transparently gateways between DBus and MQTT. 49 | 50 | The FlashMQ server runs once per user because the plug-in access the 51 | Session DBus. By default it binds to port `51883+$SCREEN` 52 | 53 | On The Venusian, installing this server is optional (for now). 54 | 55 | The best way to integrate this MQTT server with the rest of your 56 | installation is to run an independent system-level MQTT service. You need 57 | to teach that server to listen to port 51883. 58 | 59 | 60 | ### Change the VNC port 61 | 62 | If port 1 is in use: 63 | 64 | * `echo SCREEN=3 >>/etc/venusian/venus/vars` 65 | * `systemctl restart user@venus` 66 | 67 | ## Background 68 | 69 | Venus binaries run on the host system, using QEMU if necessary. 70 | 71 | The Venus subsystem uses a session DBUS, not the system's. 72 | 73 | Its programs run as the user "venus", not root. 74 | 75 | The Venus system does *not* run inside a chroot environment or a container. 76 | 77 | ### OpenEmbedded libraries 78 | 79 | Venus (yes, even on Raspberry Pi 4) is an `armhf` system; its binaries thus 80 | use `/lib/ld-linux-armhf.so.3` as their ELF loader. 81 | 82 | The Venusian assumes that its host is not an `armhf` system 83 | and doesn't need to run any other `armhf` binaries. 84 | 85 | The Venusian hijacks this `armhf` loader by patching the `/lib` and 86 | `/usr/lib` strings that define its library search path, to `/v/l/` and 87 | `/v/u/lib` respectively. Appropriate symlinks then ensure that the Venus 88 | binaries use Venus libraries. 89 | 90 | This means that your root directory will gain `/v` and `/data` entries. 91 | Sorry about that; ways around this problem are being investigated. 92 | 93 | We also need to add two symlinks to `/usr/lib` (`gconv` and `fonts`) 94 | to convince `vedirect` and `gui` to not crash. These directories should not 95 | exist on modern Debian systems, so there's no problem. 96 | 97 | 98 | #### Alternate solution 99 | 100 | If you do need to run non-Venus `armhf` binaries, we could alternately 101 | modify the Venus binaries so their loader is in `/v/l/` instead of `/lib/`. 102 | 103 | Patches welcome. 104 | 105 | 106 | ### Venusian user 107 | 108 | The Venusian system runs as the user `venus`. It's controlled by a 109 | user-level systemd instance and uses that user's session dbus instead of 110 | the system's. 111 | 112 | Thus, a simple `systemctl restart user@venus` restarts the whole Venus 113 | subsystem cleanly, without requiring a possibly-risky reboot that takes an 114 | order of magnitude longer and is much more disruptive. 115 | 116 | 117 | ### Devices ("udev") 118 | 119 | Venus handles new serial devices by creating a symlink in /etc/serial-probe. 120 | That directory is polled by a background process every two seconds. The 121 | serial line is probed; if successful, the appropriate handler is created 122 | and started via svc/demontools. 123 | 124 | The Venusian considers this to be broken. 125 | 126 | * Polling is bad. Don't do it. 127 | * Many cheap adapters don't have serial numbers. You need to distinguish 128 | them somehow. 129 | * Probing is time-consuming (multiple baud rates, protocols, device addresses 130 | on modbus, waiting for replies, starting multiple testers …). 131 | * Probe messages can (and sometimes do) confuse the targets, esp. when sent 132 | at the "wrong" baud rate. 133 | * There's no way to easily adjust the process, e.g. unconventional baud 134 | rates for Modbus/RTU. 135 | * The probed device might be busy or booting up, thus not react in time. 136 | * Devices that go away may or may not terminate the job started for them. 137 | 138 | Our solution is different. 139 | 140 | * New devices are handled by sending a message to a Unix socket. 141 | (Existing ones still use the symlink directory, for convenience.) 142 | * The device handler reads a database of known devices, identified by serial 143 | number or bus position or whatnot. It then starts a systemd unit instance 144 | with the device (plus an opional parameter) as the instance name. 145 | * Likewise, when a device goes away, its systemd unit is stopped. 146 | * The program started by this, commonly a shell script, is responsible for 147 | executing whatever command the Victron probe system would have started. 148 | * TODO: a helper to determine what the original serial prober would 149 | have done. 150 | 151 | 152 | ### Multi-site operation 153 | 154 | It is possible to run more than one Venus instance on the same host, 155 | mostly thanks to systemd magic that bind-mounts the `/var/lib/venusian/NAME` 156 | directory to `/data`. 157 | 158 | Venus' MQTT topics are prefixed by the output of `get-unique-id`, which 159 | returns the system's UUID. We modify this script to include the user ID. 160 | 161 | 162 | ### Port redirects 163 | 164 | The Venusian sets up a couple of `iptables` rules so that the user's MQTT 165 | clients are served by its own MQTT server, not the system's. 166 | 167 | As a side effect the user's MQTT server can't talk to the system server's 168 | port 1883. The workaround is to teach the latter to also listen on port 169 | 51883. 170 | 171 | 172 | ## Installation 173 | 174 | The `install` script copies a current Venus image to a subvolume or 175 | subdirectory of the host system. 176 | 177 | The script accepts a couple of arguments. `dir`, `root` and `mount` are 178 | mandatory. You need to run it as root. 179 | 180 | The Venus installation will be copied to the host system as-is. It won't 181 | be modified. 182 | 183 | 184 | ### --image=/path/to/venus.img 185 | 186 | The Venus image to use. Skip this option if you already unpacked or mounted 187 | your Venus image. 188 | 189 | The file may be gzip-compressed; in this case you'll need sufficient space 190 | in /tmp for an uncompressed copy. 191 | 192 | The special value "--image=-web-" downloads the current Venus version. 193 | The image will be deleted after it's unpacked. 194 | 195 | 196 | ### --dest=/path/to/dir 197 | 198 | The directory where you want to save (or did save) the Venus image to. 199 | This option is mandatory unless you're creating a new Venusian user. 200 | 201 | ### --root=/path/to/debian 202 | 203 | The Debian installation to install to. This may be the currently-running 204 | system (use "/"). 205 | 206 | ### --mount=/mnt/venus 207 | 208 | The directory which `--dir` will be mounted to or accessible at. 209 | 210 | This path is relative to `--root`. 211 | 212 | If you skip this option *and* the root is `/`, the path from `--dir` will be used. 213 | Otherwise you'll need to provide it. 214 | 215 | ### --quiet 216 | 217 | Don't report print the script is doing (high-level). 218 | 219 | ### --verbose 220 | 221 | Report shell commands as the installer executes them. 222 | 223 | ### --skip 224 | 225 | Skip existing files. (By default, everything is overwritten.) 226 | 227 | ### --sub=WHAT 228 | 229 | Also run the named add-on, stored in the directory `install.d/WHAT`. 230 | This option may be used multiple times. 231 | 232 | This allows you to customize the installation without modifying 233 | the base script. See the "Customization" section, below, for details. 234 | 235 | ### --help 236 | 237 | Prints a summary of the options given above, as well as a list of possible 238 | add-ons. 239 | 240 | ## Installation errors 241 | 242 | Note that the destination directory contains the *unmodified* Venus sources. 243 | Any changes which the installer applies are done with an overlay directory. 244 | 245 | ### Unknown startup script 246 | 247 | Read the script (in `$DEST/etc/rcS.d` or `$DEST/etc/rc5.d`) to check what it does. 248 | 249 | Open the `install` script, find the line `### Startup scripts`. If it does 250 | something The Venusian needs to replicate, write a service file and add it 251 | to our `service` directory. Otherwise, simply add a do-nothing entry with 252 | a short comment. 253 | 254 | ### Patch failures 255 | 256 | The Venusian handles multiple versions of the Venus source files. 257 | To handle a conflict: 258 | 259 | * create a directory 'patchname.V' if necessary 260 | * copy the old patch to 'patchname.V/vX.YZ' (with X.YZ being the last Venus 261 | release to which it applied cleanly) 262 | * fix the patch 263 | * commit the files to git, create a pull request 264 | 265 | 266 | ## Examples 267 | 268 | ### Raspberry Pi SD Card 269 | 270 | Let's assume you have an SD card for a Raspberry Pi 4 you'd like to Venusianize. 271 | 272 | * Copy a [Debian image](https://raspi.debian.net/tested-images/) to the card 273 | * Plug into your computer; ensure that the card is **not** auto-mounted 274 | * Use parted or fdisk to add a new partition at the end (1 GByte is sufficient) 275 | * grow the second partition ("RASPIROOT") to fill all available space 276 | * `resize2fs /dev/sdX2` (whichever sdX device your card is) 277 | * `mkfs.ext4 -L VENUS /dev/sdX3` 278 | * mount the partitions; we'll assume you use `/mnt/part2` as the target 279 | * `install -i /tmp/venus.img.gz -d /mnt/part2 -r /mnt/venus 280 | * Add `LABEL=VENUS /mnt/venusian ext4 none 0 2` to `/mnt/part2/etc/fstab` 281 | * unmount, eject, plug into Raspberry Pi, etc.. 282 | 283 | ### A typical server 284 | 285 | * Use Debian >=13. 286 | * `git clone https://github.com/M-o-a-T/venusian.git /opt/venusian` 287 | * `cd /opt/venusian` 288 | * `./install -i /tmp/venus.img.gz -d /opt/venus -r / 289 | * systemctl start user@venus 290 | * vncclient localhost:1 291 | 292 | 293 | ## helper scripts 294 | 295 | All scripts are located in `/usr/lib/venusian/bin`. You can add this 296 | directory to your path, or use shell aliases. 297 | 298 | ### get-unique-id 299 | 300 | This is the script Venus uses to generate a unique ID, originally by using 301 | the MAC address of `eth0`. 302 | 303 | There are problems with this approach; some hosts don't have an `eth0`, 304 | others might use a dynamic MAC for privacy. It's also not multi-user capable. 305 | 306 | The Venusian uses a prefix of the machine ID and adds a the user ID (in hex). 307 | You can add a "MACHID=XXX" line to `/etc/venusian/vars` or `/etc/venusian/NAME/vars` 308 | if you want to override it. Note that the latter file includes the user ID. 309 | 310 | ### ven 311 | 312 | `ven XXX` runs `XXX` as the user "venus". You can use "-u NAME" to switch to 313 | a different user. 314 | 315 | This helper is required because a mere `sudo -u venus` doesn't connect you to 316 | the user's DBus. 317 | 318 | ### vctl 319 | 320 | Alias to `ven systemctl --user`. The "-u NAME" option is accepted. 321 | 322 | ### vbus 323 | 324 | Alias to `ven dbus`. The "-u NAME" option is accepted. 325 | 326 | "dbus" is a script from Victron that's much nicer than the 327 | standard dbus-send program. 328 | 329 | 330 | ## Environment settings 331 | 332 | 333 | ## File system structure 334 | 335 | The Venusian does not change the Venus file system. It needs to fix a 336 | few minor problems, but that's with by an overlay file system. 337 | 338 | We do however need to add a few symlinks to the file system's root, and to /usr. 339 | 340 | 341 | ## Customization 342 | 343 | You can add your own customization steps to a directory `install.d/NAME`. 344 | It should contain bash scriptlets that are executed within the `install` script. 345 | 346 | ### Scriptlets 347 | 348 | These customizer scriptlets are used: 349 | 350 | #### pre 351 | 352 | Runs first. 353 | 354 | #### post 355 | 356 | Runs last. 357 | 358 | #### pkg-r 359 | 360 | Packages to install in the root. 361 | 362 | Add them to the `$I` variable. Please try to test whether they're already there, 363 | because if so we can skip the time-consuming `apt` step. 364 | 365 | #### lib 366 | 367 | If this script is supplied, it replaces the built-in method that creates a 368 | hacked-up copy of the armhf ELF loader. You could use this to implement the 369 | "patch Venus" strategy, as mentioned above. 370 | 371 | 372 | ### Variables 373 | 374 | * R: the destination Debian system's root 375 | * V: the Venus subdirectory (a copy of the whole Venus image) 376 | * MNT: the mountpoint of $V on $R, typically /mnt/venus 377 | * LIBV: /var/lib/venus (the Victron user's home directory) 378 | * USRV: /usr/lib/venusian (common helper files and scripts) 379 | * SUBS: array of scriptlets to run 380 | * OVER: the overlay file system 381 | * FORCE: unconditionally replace things 382 | 383 | Remember that all of these may contain spaces or other special characters. 384 | While we recommend to use paths without whitespace, it's still good practice 385 | to quote **all** uses of these variables. 386 | 387 | 388 | #### Overlay file system 389 | 390 | The installer does not create a complete copy of `/opt/victronenergy` (the 391 | directory Victron ships its code in). Instead, an overlay file system is 392 | used. `$OVER` contains the path to the "upper directory" of the overlay 393 | file system that we use to selectively alter files. 394 | 395 | However, this might change, as userspace overlays impose a certain 396 | runtime overhead. 397 | 398 | Thus, a test whether to replace a file with a patched version should succeed if 399 | any of 400 | 401 | * `$FORCE` is set 402 | * the destination doesn't exist 403 | * source and destination files are identical 404 | * the source is newer 405 | 406 | It should always `rm -f` the destination and `mkdir -p` any intermediate 407 | directories. 408 | 409 | The helper function `fchg ‹source› ‹dest›` does this for you. It exits with 410 | return code 1 if you don't need to do anything. Otherwise the destination 411 | is a new empty file. 412 | 413 | The helper `fln` creates a symlink at ‹dest› that points to ‹source›. 414 | 415 | The overlay file system is mounted with an `opt-victronenergy.mount` systemd unit. 416 | 417 | 418 | ## Changes to the host system 419 | 420 | This section broadly documents the install script's changes to the host filesystem. 421 | 422 | * create and populate $V (350 MBytes; small image as of 2024-01) 423 | * create a user and group "venusian" 424 | * mount its overlay file system at `/opt/victronenergy` 425 | * populate its home directory in $LIBV 426 | * create a patched `/lib/ld-linux-armhf.so.3` 427 | * create a directory `/v` (with library symlinks, for the loader) 428 | * create a symlink `/data` (to $LIBV) 429 | * populate $USRV 430 | * add udev scripts 431 | * add helpers with hardcoded paths: 432 | * get-unique-id (uses the host's machine ID) 433 | 434 | ## TODOs 435 | 436 | Last but definitely not least, this is a work in progess. 437 | 438 | ### Support for more battery managers, meters, and whatnot 439 | 440 | There are sure to be some more incompatibilities. 441 | Bug reports, patches and additional code gladly accepted. 442 | 443 | 444 | ### Clean up the Venus copy 445 | 446 | Much of the original Venus file system is no longer required and can be deleted 447 | to free some space. For instance, there's 60 MBytes of kernel modules, 448 | 60 MBytes of Python 3.8, 40 MBytes of opkg data, 15 MBytes of web server content, 449 | and almost all of `/bin` and `/usr/bin`. 450 | 451 | Original image's root file system: 350 MB, 100 MB compressed; cleaned up: around 452 | 70 MBytes, uncompressed. 453 | 454 | ### Documentation 455 | 456 | This file is way too long. 457 | 458 | ### Installation 459 | 460 | When installing a specific version, find the corresponding image. This is 461 | not trivial because on Victron's server the file name has a timestamp along 462 | with the file name. 463 | -------------------------------------------------------------------------------- /doc/intro.txt: -------------------------------------------------------------------------------- 1 | TL;DR: Running Venus as part of any machine running Debian (stable) seems possible. Yes, even on x64 systems. 2 | 3 | Disclaimer, up front: yes this is a hack. It is not endorsed or supported by Victron, or anybody else (other than me) for that matter. It's also preliminary work and probably not yet(!) suited for production use. 4 | 5 | ***************** 6 | 7 | I'd like to introduce: 8 | 9 | The Venusian. https://github.com/M-o-a-T/venusian/ 10 | 11 | This repository contains an installer script (plus some support files) that convince your Venus system to run on a normal Debian system, as the user "venus". 12 | 13 | It is managed with a "systemd --user" process that starts a separate dbus session. 14 | 15 | A monitor process watches udev directly and starts/stops tasks in parallel; no venus-specific udev rules are necessary (except for adding permissions, if necessary). Logging and all other debug output goes to the systemd journal as usual. 16 | 17 | I don't like probing serial ports or ModBus registers to figure out what to do. Not on every startup. It's fragile, takes time, and spams the port with garbage. Instead, a simple YAML config file tells the monitor which unit to start, and with which parameters. 18 | 19 | As a nice side effect, cleanly restarting the whole Venus system now takes just a few seconds and doesn't require a reboot. TODO: Write a "venus-update" script does all the heavy lifting so that you can do a fast and complete roll-back if there's a problem. 20 | 21 | Why am I doing this? 22 | 23 | Venus is built on top of OpenEmbedded, which I find moderately annoying. Like, Python 3.8 is about to be end-of-life, daemontools is not my cup of tea (how *do* you send your syslog data to another server with this thing? answer, you don't) , its git version is old, its kernel doesn't support basics like i²c and Victron won't change that, everything runs as root and on the system dbus, probing RS232 may or may not work … and so on. 24 | 25 | I also have a fast NAS system (one that twiddles its thumbs) sitting right beside my PV setup. While that's an amd64 box, there's qemu userspace emulation, so … 26 | 27 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u 4 | 5 | HERE="$(realpath "$(dirname "$0")")" 6 | 7 | usage() { 8 | cat <<_END_ 9 | Usage: $0 10 | User: 11 | --user NAME 12 | The user for Venusian. If you use this option without '--dest', 13 | a new user for the existing Venus installation will be created. 14 | 15 | --vnc N 16 | Use port 5900+N for VNC. The default is 1, or the next free port 17 | as determined by the SCREEN=X entries in your /etc/venusian/*/vars 18 | files. 19 | 20 | --fb 21 | Use the system's "real" video output. 22 | 23 | System: 24 | --image A --dest B --root C --mount D 25 | 26 | '--dest' is mandatory. '--root' defaults to the current system. 27 | 28 | If '--image' is used, unpack the Venus image A into directory B. 29 | Next, modify the system at root C. The mountpoint of B on C shall be D. 30 | 31 | The image can be a URL. The special value "-web-" downloads the current image, 32 | "-rc-" uses the latest release candidate. 33 | 34 | Using "-web-3.14" to retrieve a specific version is on the TODO list. 35 | 36 | --verbose 37 | Print the steps the script executes. 38 | 39 | --quiet 40 | Do not report (coarse) progress. 41 | 42 | --skip 43 | Skip existing files. 44 | 45 | --replace 46 | Delete the existing Venusian installation before unpacking the new image. 47 | 48 | --no-update 49 | Don't update our git submodules. (Useful while developing.) 50 | 51 | --sub=ADDON 52 | Process add-on scripts. 53 | 54 | --repo SOURCE 55 | Use this URL to fetch a sources.list file for additional Debian packages. 56 | Use "--repo -" to disable. 57 | The result will be stored in /etc/apt/sources.list.d/venusian.list 58 | 59 | --repo-key PUBKEYFILE 60 | Use this URL to fetch the GPG signature file associated with REPO. 61 | 62 | Available additions: 63 | _END_ 64 | ls -1 "$HERE/install.d" | while read d ; do echo -n " $d "; 65 | READ="$HERE/install.d/$d/README" 66 | if test -f "$READ" ; then head -1 "$READ"; else echo "‹no README file›"; fi 67 | done 68 | exit 1 69 | } 70 | 71 | if [ "$(id -u)" != 0 ] ; then 72 | echo "This script needs to run as root. Sorry." >&2 73 | exit 1 74 | fi 75 | 76 | apti() { 77 | R="$1" 78 | shift 79 | $Q echo Installing $@ 80 | if test "$R" = "/" ; then 81 | apt-get install --yes --no-install-recommends -o Dpkg::Options::="--force-confdef" "$@" /dev/tty 2>&1 82 | else 83 | systemd-nspawn -D "$R" apt-get install --yes --no-install-recommends -o Dpkg::Options::="--force-confdef" "$@" /dev/tty 2>&1 84 | fi 85 | } 86 | 87 | subs() { 88 | W="$1" 89 | for S in "${SUB[@]}" ; do 90 | if test -f "$HERE/install.d/$S/$W" ; then 91 | $Q echo Processing "$S/$W" 92 | source "$HERE/install.d/$S/$W" 93 | fi 94 | done 95 | } 96 | 97 | _chg() { 98 | # check: do we replace DST from SRC? 99 | SRC="$1" 100 | DST="$2" 101 | test -n "$FORCE" && return 0 102 | test -f "$DST" || return 0 103 | test "$SRC" -ef "$DST" && return 0 104 | test "$SRC" -nt "$DST" && return 0 105 | cmp -s "$SRC" "$DST" && return 0 106 | # "cmp -s" compares sizes before reading, so we don't need to 107 | return 1 108 | } 109 | 110 | fcat() { 111 | # check: do we not replace DST? 112 | DST="$1" ; shift 113 | if [ -z "$FORCE" ] && [ -f "$DST" ] ; then cat >/dev/null ; return ; fi 114 | S1=0000 115 | if [ $# -gt 0 ] && [ -f "$DST" ] ; then 116 | S1=$(sha256sum < "$DST" | sed -e 's/ .*//') 117 | fi 118 | 119 | cat >"$DST" 120 | if [ $# -gt 0 ] && [ $S1 != $(sha256sum < "$DST" | sed -e 's/ .*//') ] ; then 121 | "$@" 122 | fi 123 | } 124 | 125 | fchg() { 126 | # check: do we not replace DST from SRC? 127 | SRC="$1" 128 | DST="$2" 129 | _chg "$SRC" "$DST" || return 0 130 | rm -f "$DST" 131 | mkdir -p "$(dirname "$DST")" 132 | touch "$DST" 133 | if test -x "$SRC" ; then chmod 755 "$DST"; else chmod 644 "$DST"; fi 134 | return 1 135 | } 136 | 137 | fln() { 138 | # To link or not to link … 139 | SRC="$1" 140 | DST="$2" 141 | if test -e "$DST" && ! test -L "$DST" ; then 142 | echo "ERROR: '$DST' is not a symlink. Exiting." >&2 143 | exit 1 144 | fi 145 | if test -z "$FORCE" && test -L "$DST" ; then return ; fi 146 | rm -f "$DST" 147 | ln -sf "$SRC" "$DST" 148 | } 149 | 150 | 151 | next_vnc() { 152 | # well, "next" unless it's an existing user who already has a screen# 153 | local P=0 SC f NAME=$1 154 | while read f ; do 155 | SC="$(sed -ne 's/^SCREEN=//p' <$f)" 156 | if [ -n "$SC" ] && [ "$(basename "$(dirname "$f")")" = "$NAME" ] ; then 157 | echo $SC 158 | return 0 159 | fi 160 | test -z "$SC" || test "$SC" -le $P || P=$SC 161 | done < <( find "$R/etc/venusian" -maxdepth 2 -mindepth 2 -type f -name vars ) 162 | echo $( expr $P + 1 ) 163 | } 164 | 165 | gen_user() { 166 | local NAME="$1" LIBV=$LIBVV/$NAME 167 | 168 | if grep -qs "^$NAME:" "$R/etc/passwd" ; then 169 | $Q echo Updating user $NAME 170 | else 171 | $Q echo Generating user $NAME 172 | chroot "$R" /sbin/adduser --disabled-login --home "$LIBV" --ingroup venus --system --comment "Venus Main User" $NAME 173 | fi 174 | 175 | grep -qs "^$NAME:" "$R/etc/passwd" || chroot "$R" /sbin/adduser --disabled-login --home "$LIBV" --ingroup venus --system --comment "Venus Main User" $NAME 176 | 177 | chroot "$R" /sbin/adduser $NAME systemd-journal 2>/dev/null 178 | chroot "$R" /sbin/adduser $NAME dialout 2>/dev/null 179 | chroot "$R" /sbin/adduser $NAME bluetooth 2>/dev/null 180 | test ! -v FB || chroot "$R" /sbin/adduser $NAME video 2>/dev/null 181 | 182 | SC="$LIBV/.config/systemd/user.control" 183 | mkdir -p "$R/$SC/default.target.wants/" 184 | fln "$SERVICE" "$R/$LIBV/.config/systemd/user" 185 | fln ".config/systemd/user.control" "$R/$LIBV/service" 186 | 187 | fcat "$R/$LIBV/.config/systemd/user.conf" <<_END_ 188 | [Manager] 189 | DefaultEnvironment="PATH=$USRV/bin:/bin:/sbin" 190 | _END_ 191 | 192 | mkdir -p "$R$LIBV/"{conf,db,log,etc,themes,var/lib} 193 | 194 | USERID=$(grep "^$NAME:" $R/etc/passwd | ( IFS=: read a b c d ; echo $c ) ) 195 | env USER=$NAME UID=$USERID $USRV/bin/get-unique-id > $R$LIBV/conf/unique-id 196 | 197 | chroot "$R" chown -R $NAME $LIBV/{etc,conf,db,log,.config} 198 | 199 | VPW="$R$LIBV/conf/vncpassword.txt" 200 | if test ! -s "$VPW" ; then 201 | read -s -p "VNC password: " pwd /dev/tty 2>&1 202 | echo "$pwd" >"$VPW" 203 | pwd= 204 | fi 205 | 206 | mkdir -p "$R/$UD" 207 | mkdir -p "$R/$EV" 208 | 209 | fcat "$R/$UD/venusian.conf" <<_END_ 210 | [Unit] 211 | After=opt-victronenergy.mount $MNT_U 212 | Requires=opt-victronenergy.mount $MNT_U 213 | 214 | [Service] 215 | BindPaths=/var/lib/venusian/%i:/data 216 | BindPaths=/var/lib/venusian/%i/etc:/etc/venus 217 | 218 | EnvironmentFile=-/etc/venusian/vars 219 | EnvironmentFile=-$EV/vars 220 | 221 | ExecStartPost=+$USRV/bin/iptables-redirect on 222 | ExecStopPost=+$USRV/bin/iptables-redirect off 223 | _END_ 224 | 225 | if [ ! -s "$R/$EV/vars" ] || ! grep -qs "^SCREEN=" "$R/$EV/vars" ; then 226 | echo SCREEN=$SCREEN >>"$R/$EV/vars" 227 | fi 228 | if [ -v FB ] ; then 229 | echo FB=y >>"$R/$EV/vars" 230 | fi 231 | 232 | subs user 233 | } 234 | 235 | # command line processing 236 | TEMP=$(getopt -o 'fFi:d:hm:nqr:Rs:u:v?' --long 'dest:,fast,fb,force,help,image:,mount:,no-update,replace,repo:,repo-key:,root:,skip,sub:,user:,verbose' -n "$0" -- "$@") 237 | if [ $? -ne 0 ]; then 238 | usage >&2 239 | fi 240 | 241 | set -e 242 | eval set -- "$TEMP" 243 | unset TEMP 244 | 245 | Q= 246 | verb= 247 | root=/ 248 | FORCE=y 249 | SUB=() 250 | SUB_UPD=y 251 | REPO=http://build.smurf.noris.de/info/smurf.list 252 | REPOKEY=http://build.smurf.noris.de/info/smurf.archive.gpg 253 | replace= 254 | unset NAME DEST MNT img fast FB 255 | 256 | while : ; do 257 | case "$1" in 258 | (-h|-\?|--help) 259 | usage; 260 | exit 0 ;; 261 | (-q|--quiet) 262 | Q=":" 263 | shift ;; 264 | (-f|--force) 265 | FORCE=y 266 | shift ;; 267 | (-F|--fast) 268 | fast=y 269 | shift ;; 270 | (--fb) 271 | FB=y 272 | shift ;; 273 | (--skip) 274 | FORCE="" 275 | shift ;; 276 | (-n|--no-update) 277 | SUB_UPD= 278 | shift ;; 279 | (-v|--verbose) 280 | verb=y 281 | shift ;; 282 | (-R|--replace) 283 | replace=y 284 | shift ;; 285 | (-u|--user) 286 | NAME="$2" 287 | shift 2 ;; 288 | (-d|--dest) 289 | DEST="$2" 290 | shift 2 ;; 291 | (-i|--img|--image) 292 | img="$2" 293 | shift 2 ;; 294 | (-r|--root) 295 | root="$2" 296 | shift 2 ;; 297 | (-m|--mount) 298 | MNT="$2" 299 | shift 2 ;; 300 | (--repo) 301 | REPO="$2" 302 | shift 2 ;; 303 | (--repo-key) 304 | REPOKEY="$2" 305 | shift 2 ;; 306 | (-s|--sub) 307 | if test -d "$HERE/install.d/$2" ; then 308 | SUB+=("$2") 309 | else 310 | echo "Unknown argument ('$2'). Exiting." >&2 311 | fi 312 | shift 2 ;; 313 | (--) 314 | shift; break ;; 315 | (*) 316 | echo "Internal error! '$1'" >&2 317 | exit 1 318 | esac 319 | done 320 | 321 | 322 | temp=$(mktemp -d) 323 | trap 'rm -r $temp' 0 1 2 15 324 | cd $temp 325 | umask 022 326 | 327 | R="$(realpath "$root")" 328 | 329 | LIBVV=/var/lib/venusian 330 | USRV=/usr/lib/venusian 331 | SERVICE="$USRV/service/" 332 | 333 | if [ ! -v MNT ] ; then 334 | MNT="${DEST:-/}" 335 | fi 336 | 337 | if [ -v NAME ] ; then 338 | if [ ! -v DEST ] ; then 339 | if [ ! -s /etc/systemd/system/opt-victronenergy.mount ] ; then 340 | echo "Adding users requires an existing Venusian installation." >&2 341 | usage >&2 342 | exit 2 343 | fi 344 | gen_user $NAME 345 | exit 0 346 | fi 347 | else 348 | NAME=venus 349 | fi 350 | 351 | SCREEN=$(next_vnc $NAME) 352 | MNT_U=$(systemd-escape -m "$MNT") 353 | 354 | UD="/etc/systemd/system/user@$NAME.service.d/" 355 | EV="/etc/venusian/$NAME" 356 | 357 | if [ ! -v DEST ] ; then 358 | echo "A directory for/with the Venus file system is required." >&2 359 | echo "(Use the '--dest PATH' option.)" >&2 360 | echo "" >&2 361 | usage >&2 362 | exit 2 363 | fi 364 | 365 | if test "$verb" = "y" ; then 366 | Q=":" 367 | set -x 368 | fi 369 | 370 | if [ $# -gt 0 ] ; then 371 | echo "Superfluous arguments. Exiting." >&2 372 | exit 2 373 | fi 374 | 375 | if [ -d "$USRV" ]; then 376 | if [ -z "$replace" ] ; then 377 | echo "Existing installation found ($USRV). Use '--replace'." >&2 378 | exit 2 379 | fi 380 | echo -n "Stopping Venusian users: " 381 | ls /etc/venusian | while read u ; do 382 | systemctl stop user@$u || true 383 | echo -n "$u " 384 | done 385 | echo "." 386 | umount /opt/victronenergy || true 387 | rm -r "$USRV" 388 | fi 389 | 390 | if [ -d "$DEST" ] && [ -v img ]; then 391 | if [ -z "$replace" ] ; then 392 | echo "Existing image found ($DEST). Either add '--replace', or don't use '--image'." >&2 393 | exit 2 394 | fi 395 | btrfs subvol delete "$DEST" 2>/dev/null || rm -rf "$DEST" 396 | fi 397 | 398 | ################# install local requirements 399 | I="" 400 | which systemd-nspawn >/dev/null 2>&1 || I="$I systemd-container" 401 | which rsync >/dev/null 2>&1 || I="$I rsync" 402 | if test -n "$I" ; then 403 | $Q echo Locally installing $@ 404 | apt install $I 405 | fi 406 | 407 | if test -n "$SUB_UPD" ; then 408 | ( 409 | cd "$HERE" 410 | git submodule update --init --recursive 411 | ) 412 | fi 413 | 414 | ################# COPY 415 | 416 | if test -v img ; then 417 | case "$img" in 418 | (http:*|https:*) DL="$img" ;; 419 | (-web-) DL=http://updates.victronenergy.com/feeds/venus/release/images/raspberrypi4/venus-image-raspberrypi4.wic.gz ;; 420 | (-rc-) DL=http://updates.victronenergy.com/feeds/venus/candidate/images/raspberrypi4/venus-image-raspberrypi4.wic.gz ;; 421 | # (-web*-) DL=?? ;; # TODO get the link from somewhere 422 | esac 423 | fi 424 | 425 | if test -v DL ; then 426 | F="$temp/img" 427 | if [ "${DL#.gz}" = "$DL" ] ; then 428 | wget -O - "$DL" | gzip -d > "$F" 429 | else 430 | wget -O $F "$DL" 431 | fi 432 | img="$F" 433 | fi 434 | 435 | if [ ! -v img ] ; then 436 | $Q echo "No image given; not copying." 437 | else 438 | $Q echo Copying image 439 | 440 | if test -d "$DEST" ; then 441 | : 442 | else 443 | btrfs subvol cre "$DEST" 2>/dev/null || mkdir "$DEST" 444 | fi 445 | 446 | # if the image is compressed, unpack it 447 | imgz="$img" 448 | img="${imgz%.gz}" 449 | if [ "$img" != "$imgz" ] ; then 450 | img="$temp/${img##*/}" 451 | gzip -d < "$imgz" > "$img" 452 | fi 453 | 454 | mkdir $temp/dir 455 | ld=$(losetup -f --show -P "$img") 456 | trap 'losetup -d $ld; rm -r $temp' 0 1 2 15 457 | 458 | mount ${ld}p2 $temp/dir 459 | trap 'umount $temp/dir; losetup -d $ld; rm -r $temp' 0 1 2 15 460 | 461 | rsync -a --numeric-ids --perms --inplace "$temp/dir/." "$DEST/." 462 | echo "Copy/sync finished." 463 | 464 | umount $temp/dir; losetup -d $ld 465 | trap 'rm -r $temp' 0 1 2 15 466 | fi 467 | ################# END COPY 468 | 469 | 470 | ################# SETUP 471 | if ! VERS=$(sed -ne 's/.* \(v[0-9]\.[0-9][0-9]*\).*/\1/p' <"$DEST/etc/issue") ; then 472 | echo Could not detect the Venus version in "'$DEST/etc/issue'" >&2 473 | exit 1 474 | fi 475 | if test -f "$root" ; then 476 | echo TODO root as an Image >&2 477 | exit 1 478 | fi 479 | 480 | DEST="$(realpath "$DEST")" 481 | 482 | mkdir -p "$R/opt/victronenergy" 483 | mkdir -p "$R/etc/venus" 484 | mkdir -p "$R/data" 485 | mkdir -p "$R/$USRV"/{opt,.opt} 486 | 487 | subs pre 488 | 489 | # create user 490 | $Q echo Primary set-up 491 | grep -qs "^venus:" "$R/etc/group" || chroot "$R" /sbin/addgroup --system venus 492 | grep -qs "^venusian:" "$R/etc/passwd" || chroot "$R" /sbin/adduser --disabled-login --home "$LIBVV" --ingroup $NAME --system --comment "Venusian System" venusian 493 | 494 | # systemd 495 | mkdir -p "$SERVICE" 496 | 497 | # set up DBUS permissions 498 | for f in "$DEST/etc/dbus-1/system.d"/* ; do 499 | g="$R/etc/dbus-1/system.d/${f##*/}" 500 | test -s "$g" || \ 501 | sed <"$f" >"$g" -e 's/"root"/"venus"/' 502 | done 503 | 504 | cp -r "$HERE/lib/"* "$R$USRV/" 505 | 506 | if [ "X$REPO" != "X-" ] ; then 507 | $Q echo "Adding repository (some packages are not yet in Debian)" 508 | 509 | test -s "$R/etc/apt/trusted.gpg.d/venusian.archive.gpg" || wget -q -O "$R/etc/apt/trusted.gpg.d/venusian.archive.gpg" $REPOKEY 510 | test -s "$R/etc/apt/sources.list.d/venusian.list" || wget -q -O "$R/etc/apt/sources.list.d/venusian.list" $REPO 511 | fi 512 | 513 | if ! test -v fast ; then 514 | if test "$R" = "/" ; then 515 | apt update 516 | else 517 | systemd-nspawn -D "$R" apt update 518 | fi 519 | fi 520 | 521 | $Q echo Checking startup scripts 522 | 523 | lsrc() { 524 | ls "$DEST/etc/rcS.d" 525 | ls "$DEST/etc/rc5.d" 526 | } 527 | # not using the obvious (ls;ls)| pipe here because of a bash bug: 528 | # line numbers on errors below this point would be wrong 529 | lsrc | while read f ; do 530 | f="${f%.sh}" 531 | case "$f" in 532 | ### Startup scripts, rcS.d 533 | (S01keymap) ;; # host OS 534 | (S02sysfs) ;; # mounts a bunch of directories; host OS 535 | (S02zzz-resize-sdcard) ;; # host OS 536 | (S03mountall) ;; # host OS 537 | (S03test-data-partition) ;; # host problem 538 | (S04udev) ;; # host 539 | (S05checkroot) ;; # host 540 | (S0?modutils) ;; # host 541 | (S06alignment) ;; # host; mode 3 for alignment fixes (repair+complain) 542 | (S06checkroot) ;; # host 543 | (S06devpts) ;; # host 544 | (S07bootlogd) ;; # journal 545 | (S10overlays) ;; # done later 546 | (S20static-nodes) ;; # systemd 547 | (S29read-only-rootfs-hook) ;; # volatile /var/lib 548 | (S30clean-data) ;; # drop large log file 549 | (S36bootmisc) ;; # various system stuff 550 | (S37populate-volatile) 551 | # systemd 552 | # TODO set up /data 553 | ;; 554 | (S38dmesg) ;; # journald 555 | (S38urandom) ;; # random seed. Host. 556 | (S39hostname) ;; # host 557 | (S40read-eeprom) ;; # we don't have that 558 | (S50iptables) ;; # host 559 | (S80watchdog) ;; # possible TODO 560 | (S90gpio_pins) ;; # empty list 561 | (S90machine-conf) ;; # not on RPi 562 | (S99custom-rc-early) 563 | # TODO run /data/rcS.local 564 | ;; 565 | 566 | ### Startup scripts, rc5.d 567 | (S01networking) ;; # systemd-networkd or whatever 568 | (S02dbus-1) ;; # in base system 569 | (S09haveged) ;; # obsolete since kernel 5.6 570 | (S15mountnfs) ;; # system 571 | (S20apmd) ;; # not applicable on Raspberry Pi 572 | (S20bluetooth) ;; # host system 573 | (S20dnsmasq) ;; # host system 574 | (S20syslog) ;; # host system 575 | (S21avahi-daemon) 576 | ## host system, but ... 577 | mkdir -p "$R/etc/avahi" 578 | if test ! -f "$R/etc/avahi/avahi-daemon.conf" ; then 579 | $Q echo Installing avahi-daemon 580 | apti "$R" avahi-daemon 581 | fi 582 | sed -i -e s/^use-iff-running=/use-iff-running=yes/ "$R/etc/avahi/avahi-daemon.conf" 583 | 584 | ## TODO Garmin service file? 585 | ;; 586 | 587 | (S30update-data) ;; # TODO automatic data update 588 | (S60php-fpm) ;; # possibly some TODO or other 589 | (S70connman) ;; # host problem 590 | (S70swupdate) ;; # empty 591 | (S75avahi-autoipd) ;; # host problem 592 | (S80resolv-watch) ;; # systemd-resolved or some other host program 593 | (S82report-data-failure) ;; # local file system monitoring is not a Venus problem 594 | (S90crond) 595 | ## host OS 596 | # one builtin cron script, which does automated software updates, 597 | # which we don't do 598 | ;; 599 | 600 | (S95svscanboot) 601 | # SVC 602 | # TODO convert to systemd? 603 | ;; 604 | (S98scan-versions) ;; # alternate root file systems 605 | (S99check-updates) ;; # system update check 606 | (S99custom-rc-late) 607 | # TODO run /data/rc.local 608 | ;; 609 | (S99rmnologin) ;; # systemd 610 | (S99stop-bootlogd) ;; # journald 611 | 612 | (*) 613 | cat <<_ >/dev/stderr 614 | UNKNOWN: startup script: $f 615 | 616 | Please read the "Errors" section of our README. 617 | _ 618 | exit 1 ;; 619 | esac 620 | done 621 | 622 | ################# Services 623 | 624 | fchg "$HERE/udev.yml" "$R/$USRV/udev.yml" || \ 625 | cp "$HERE/udev.yml" "$R/$USRV/udev.yml" 626 | test -s "$R/$LIBVV/udev.yml" || 627 | cp "$HERE/udev.example.yml" "$R/$LIBVV/udev.yml" 628 | for f in $(ls "$HERE/service/") ; do 629 | fchg "$HERE/service/$f" "$R/$USRV/service/$f" || \ 630 | sed -e "s#@USRV@#$USRV#g" -e "s#@LIBVV@#$LIBVV#g" \ 631 | < "$HERE/service/$f" > "$R/$USRV/service/$f" 632 | done 633 | 634 | WANTS="$SERVICE/default.target.wants" 635 | $Q echo Converting services 636 | mkdir -p "$WANTS" 637 | starters() { 638 | ls -1 "$DEST/opt/victronenergy/service/" 639 | echo "flashmq" 640 | } 641 | starters | while read f ; do 642 | fx="${f##*/}" 643 | g="$SERVICE/$fx.service" 644 | test -n "$FORCE" || test ! -f "$g" || continue 645 | U=$temp/unit 646 | echo >$U "[Service]" 647 | 648 | case "$fx" in 649 | (ppp) continue ;; # Of course not. 650 | (llmnrd) continue ;; # Not that either. 651 | (simple-upnpd) continue ;; # no 652 | (netmon) continue ;; # avahi restart on network change 653 | (nginx) continue ;; # TODO teach the host OS 654 | (serial-starter) # that's the job of our udev-handler 655 | fln "$SERVICE/udev-handler.service" "$WANTS/udev-handler.service" 656 | continue ;; 657 | (dbus-digitalinputs) continue ;; # TODO? 658 | (dbus-qwacs) continue ;; # ? 659 | (dbus-fronius) continue ;; # ? 660 | (dbus-adc) continue ;; # ? 661 | (dbus-ble-sensors) continue ;; # ? 662 | (venus-html5-logger) continue ;; # what for? 663 | (dbus-mqtt) continue ;; # superseded by dbus-flashmq 664 | (flashmq) 665 | echo >>$U LimitNOFILE=65536 666 | echo >>$U ExecStartPre=/usr/lib/venusian/bin/gen-flashmq-conf 667 | echo >>$U ExecStart=/usr/bin/flashmq --config-file /run/user/%U/flashmq.conf 668 | echo >>$U ExecReload=/bin/kill -HUP \$MAINPID 669 | ;; 670 | (gui) 671 | echo >>$U WorkingDirectory="/opt/victronenergy/gui" 672 | echo >>$U ExecStart="$USRV/bin/start-gui" 673 | ;; 674 | (*) 675 | echo >>$U ExecStart=/opt/victronenergy/service/$fx/run 676 | ;; 677 | esac 678 | echo >>$U "Type=simple" 679 | echo >>$U "Restart=always" 680 | echo >>$U "RestartSec=10" 681 | echo >>$U "EnvironmentFile=-/etc/venusian/vars" 682 | echo >>$U "EnvironmentFile=-/etc/venusian/%u/vars" 683 | echo >>$U "EnvironmentFile=-/etc/venusian/%u/%N" 684 | echo >>$U "EnvironmentFile=-/run/user/%U/vars" 685 | echo >>$U "EnvironmentFile=-/run/user/%U/%N" 686 | if [ "$fx" != "localsettings" ] ; then 687 | echo >>$U "" 688 | echo >>$U "[Unit]" 689 | echo >>$U "Requires=localsettings.service" 690 | echo >>$U "After=localsettings.service" 691 | fi 692 | if [ "$fx" == "flashmq" ] ; then 693 | echo >>$U "Description=Per-User FlashMQ MQTT server" 694 | echo >>$U "ConditionEnvironment=MQTT" 695 | echo >>$U "" 696 | echo >>$U "[Install]" 697 | echo >>$U "WantedBy=multi-user.target" 698 | fi 699 | fcat "$SERVICE/$fx.service" <$U 700 | fln "$SERVICE/$fx.service" "$WANTS/$fx.service" 701 | 702 | rm $U 703 | done 704 | 705 | ################# ELF starter 706 | 707 | LA="$DEST/lib/ld-2.31.so" 708 | LB="$R/lib/ld-linux-armhf.so.3" 709 | L= 710 | for S in "${SUB[@]}" ; do 711 | test -s "$S/lib" || continue 712 | L=y 713 | source "$S/lib" 714 | done 715 | 716 | if test -n "$L" ; then 717 | $Q echo Skip patching ld-linux-armhf 718 | elif test ! -s "$LB" || test "$LA" -nt "$LB" ; then 719 | # need to hack 720 | $Q echo Patching ld-linux-armhf 721 | sed -e 's#/lib/#/v/l/#' -e 's#/usr/lib/#/v/u/lib/#' < "$LA" > "$LB.n" 722 | if test $(stat --format %s "$LA") -ne $(stat --format %s "$LA") ; then 723 | echo Patching $LA to $LB did not work 724 | exit 1 725 | fi 726 | mv "$LB.n" "$LB" 727 | touch -r "$LA" "$LB" 728 | chmod 755 "$LB" 729 | fi 730 | 731 | ################# Networking 732 | 733 | fcat "$R/etc/sysctl.d/10-venusian.conf" <<_END_ 734 | net.ipv4.ip_forward=1 735 | net.ipv4.conf.all.route_localnet=1 736 | net.ipv4.conf.default.route_localnet=1 737 | _END_ 738 | 739 | ################# random directories and links 740 | $Q echo Checking symlinks 741 | fln "$MNT/lib" "$R/l" 742 | fln "lib" "$DEST/usr/l" 743 | # fln "$MNT/opt/victronenergy" "$R/opt/victronenergy" 744 | test -e "$R/opt/victronenergy" || mkdir -p "$R/opt/victronenergy" 745 | fln "$MNT/usr/lib/fonts" "$R/usr/lib/fonts" 746 | fln "$MNT/usr/lib/gconv" "$R/usr/lib/gconv" 747 | 748 | # if /o/v is a mountpoint, assume that it's our overlay, 749 | # thus we write to that. Otherwise we write to the overlay's 750 | # top directory. 751 | if mountpoint -q "$R/opt/victronenergy" ; then 752 | if ! [ -d "$R/opt/victronenergy/gui" ] ; then 753 | echo "'$R/opt/victronenergy' is mounted but empty. Unmounting." >&2 754 | umount "$R/opt/victronenergy" 755 | OPTVIC="$R/$USRV/opt" 756 | else 757 | OPTVIC="$R/opt/victronenergy" 758 | fi 759 | else 760 | OPTVIC="$R/$USRV/opt" 761 | fi 762 | 763 | mkdir -p "$R/v" 764 | chmod 755 "$R/v" 765 | fln "$MNT/lib" "$R/v/l" 766 | fln "$MNT/usr" "$R/v/u" 767 | 768 | mkdir -p "$OPTVIC/gui" 769 | chmod 755 "$OPTVIC/gui" 770 | 771 | I="" 772 | case "$(chroot "$R" dpkg --print-architecture)" in 773 | (arm64|armhf) 774 | # nothing to do 775 | ;; 776 | (*) 777 | test -s "$R/usr/bin/qemu-arm" || I="$I qemu-user" 778 | test -s "$R/usr/share/binfmts/qemu-arm" || I="$I qemu-user-binfmt" 779 | ;; 780 | esac 781 | 782 | test -s "$R/usr/bin/pstree" || I="$I psmisc" 783 | test -s "$R/usr/bin/sudo" || I="$I sudo" 784 | test -s "$R/usr/bin/make" || I="$I make" 785 | test -s "$R/usr/sbin/iptables" || I="$I iptables" 786 | test -s "$R/usr/bin/socat" || I="$I socat" 787 | test -s "$R/usr/bin/bluetoothctl" || I="$I bluez" 788 | test -s "$R/usr/bin/fuse-overlayfs" || I="$I fuse-overlayfs" 789 | test -d "$R/usr/lib/python3/dist-packages/dbus_next" || I="$I python3-dbus-next" 790 | test -d "$R/usr/lib/python3/dist-packages/paho/mqtt" || I="$I python3-paho-mqtt" 791 | test -d "$R/usr/lib/python3/dist-packages/serial_asyncio" || I="$I python3-serial-asyncio" 792 | 793 | for P in lxml dbus pyudev pymodbus dnslib websockets click asyncclick asyncdbus yaml attr outcome trio ; do 794 | test -d "$R/usr/lib/python3/dist-packages/$P" && continue 795 | I="$I python3-$P" 796 | done 797 | subs pkg-r 798 | test -z "$I" || apti "$R" $I 799 | 800 | NTW="$R/etc/systemd/system/network-online.target.wants" 801 | mkdir -p "$NTW" 802 | fln /lib/systemd/system/user@.service "$NTW/user@venus.service" 803 | fln /etc/systemd/system/venus.service "$R/etc/systemd/system/network-online.target.wants/venus.service" 804 | 805 | mkdir -p "$OPTVIC/gui/qt-components/qml" 806 | fln "$MNT/usr/lib/qtopia/plugins/gfxdrivers" "$OPTVIC/gui/gfxdrivers" 807 | fln "$MNT/usr/lib/qml/QtQml" "$OPTVIC/gui/qt-components/qml/QtQml" 808 | 809 | fln "$USRV/bin/get-unique-id" "$R/sbin/get-unique-id" 810 | fln "$MNT/usr/bin/dbus" "$R/usr/bin/dbus" 811 | 812 | fln "$MNT/etc/profile.d" "$R/etc/venus/profile.d" 813 | 814 | ################# overlay 815 | 816 | # TODO check if the target system has a kernel-side overlayfs 817 | 818 | fcat "$R/etc/systemd/system/opt-victronenergy.mount" <<_END_ 819 | [Unit] 820 | After=sysinit.target 821 | Requires=sysinit.target 822 | 823 | [Mount] 824 | Where=/opt/victronenergy 825 | What=$USRV/opt 826 | Type=fuse.fuse-overlayfs 827 | Options=allow_other,lowerdir=$MNT/opt/victronenergy,upperdir=$USRV/opt,workdir=$USRV/.opt 828 | _END_ 829 | 830 | ################# patches 831 | 832 | # Patch filters are located in $HERE/patch and $HERE.install.d/NAME/patch. 833 | # Their relative path is appended to /opt/victronenergy: that file is patched. 834 | 835 | vfind() { # file 836 | local p="$1" 837 | if [ ! -d "$p.V" ] ; then echo $p; return 0; fi 838 | while read v ; do 839 | if ! [ $v \< $VERS ] ; then 840 | echo $p.V/$v; return 0 841 | fi 842 | done < <(ls "$p.V" | sort) 843 | echo $p; return 0 844 | } 845 | 846 | pat() { # path [sub] 847 | local p="$1" 848 | local f="$(realpath --relative-to "$2" "$1")" 849 | local fs="$DEST/opt/victronenergy/$f" 850 | local fd="$OPTVIC/$f" 851 | if ! test -s "$fs" ; then return; fi 852 | p="$(vfind $p)" 853 | fchg "$fs" "$fd" || \ 854 | if test -x $p ; then 855 | $p < $fs > $fd 856 | else 857 | ft="$temp/$(basename "$fd")" 858 | cp "$fs" "$ft" 859 | patch $ft < $p 860 | cp "$ft" "$fd" 861 | fi 862 | } 863 | 864 | find "$HERE/patch" -type f | while read f ; do 865 | pat "$f" "$HERE/patch" 866 | done 867 | for S in "${SUB[@]}" ; do 868 | D="$HERE/install.d/$S/patch" 869 | test -d "$D" || continue 870 | find "$D" -type f | while read f ; do 871 | pat "$f" "$D" 872 | done 873 | done 874 | 875 | 876 | ################# mountpoint 877 | 878 | if [ "$MNT" != "$DEST" ] ; then 879 | mkdir -p "$R/$MNT" 880 | 881 | if [ "$R" = "/" ] ; then 882 | fcat "$R/etc/systemd/system/$MNT_U" <<_END_ 883 | [Unit] 884 | After=sysinit.target 885 | Requires=sysinit.target 886 | 887 | [Mount] 888 | Where=$MNT 889 | What=$DEST 890 | Type=none 891 | Options=bind 892 | _END_ 893 | elif [ "$(realpath "$R/$MNT")" != "$(realpath "$DEST")" ] ; then 894 | echo "** You need to configure mounting $MNT manually **" >&2 895 | else 896 | MNT_U= 897 | fi 898 | else 899 | MNT_U= 900 | fi 901 | 902 | ################# finish 903 | 904 | find "$R/$USRV" -type d -print0|xargs -0 chmod 755 905 | find "$R/$USRV/"{bin,opt} -type f -print0|xargs -0 chmod 755 906 | 907 | gen_user $NAME 908 | 909 | subs post 910 | 911 | test "$R" = "/" && systemctl daemon-reload 912 | 913 | $Q echo "Done." 914 | -------------------------------------------------------------------------------- /install.d/build/README: -------------------------------------------------------------------------------- 1 | Install make, gcc, and friends 2 | -------------------------------------------------------------------------------- /install.d/build/pkg-r: -------------------------------------------------------------------------------- 1 | test -s "$R/usr/bin/make" || I="$I build-essential" 2 | -------------------------------------------------------------------------------- /install.d/fbset/README: -------------------------------------------------------------------------------- 1 | Install fbset called from vnc 2 | -------------------------------------------------------------------------------- /install.d/fbset/pkg-r: -------------------------------------------------------------------------------- 1 | test -s "$R/usr/bin/fbset" || I="$I fbset" 2 | -------------------------------------------------------------------------------- /install.d/flashmq/README: -------------------------------------------------------------------------------- 1 | Install the MoaT packages for battery control (TODO) 2 | -------------------------------------------------------------------------------- /install.d/flashmq/pkg-r: -------------------------------------------------------------------------------- 1 | test -s "$R/usr/bin/flashmq" || I="$I flashmq" 2 | test -s "$R/usr/libexec/flashmq/libflashmq-dbus-plugin.so" || I="$I dbus-flashmq" 3 | -------------------------------------------------------------------------------- /install.d/flashmq/post: -------------------------------------------------------------------------------- 1 | chroot "$R" systemctl enable flashmq 2 | -------------------------------------------------------------------------------- /install.d/flashmq/pre: -------------------------------------------------------------------------------- 1 | if ! grep -qs MQTT= "$R/$EV/vars" ; then 2 | port=$(( 51883 + $SCREEN )) 3 | echo MQTT=$port >> "$R/$EV/vars" 4 | fi 5 | 6 | if ! grep -qs MQTTWS= "$R/$EV/vars" ; then 7 | port=$(( 59001 + $SCREEN )) 8 | echo MQTTWS=$port >> "$R/$EV/vars" 9 | fi 10 | -------------------------------------------------------------------------------- /install.d/moat/README: -------------------------------------------------------------------------------- 1 | Install the MoaT packages for battery control (TODO) 2 | -------------------------------------------------------------------------------- /install.d/moat/pkg-r: -------------------------------------------------------------------------------- 1 | test -x "$R/usr/bin/moat" || I="$I moat" 2 | test -d "$R/usr/lib/python3/dist-packages/anyio_serial" || I="$I python3-anyio-serial" 3 | test -f "$R/usr/lib/python3/dist-packages/_pyfuse3.py" || I="$I python3-pyfuse3" 4 | for X in kv modbus mqtt micro ; do 5 | test -d "$R/usr/lib/python3/dist-packages/moat/$X" || I="$I moat-$X" 6 | done 7 | for P in asyncscope git greenback ; do 8 | test -d "$R/usr/lib/python3/dist-packages/$P" && continue 9 | I="$I python3-$P" 10 | done 11 | -------------------------------------------------------------------------------- /install.d/moat/post: -------------------------------------------------------------------------------- 1 | chroot "$R" systemctl enable moat-kv 2 | -------------------------------------------------------------------------------- /install.d/moat/pre: -------------------------------------------------------------------------------- 1 | mkdir -p "$R/etc/moat" 2 | test -s "$R/etc/moat/kv.env" || echo MODE=hybrid > "$R/etc/moat/kv.env" 3 | -------------------------------------------------------------------------------- /lib/bin/gen-flashmq-conf: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /run/user/$UID/flashmq.d 4 | 5 | if [ -v SCREEN ] ; then 6 | [ -v MQTT ] || MQTT=$(( 51880 + $SCREEN )) 7 | [ -v MQTTWS ] || MQTTWS=$(( 59000 + $SCREEN )) 8 | fi 9 | 10 | cat > /run/user/$UID/flashmq.conf <<_ 11 | thread_count 1 12 | max_packet_size 10240 13 | client_max_write_buffer_size 102400 14 | 15 | plugin /usr/libexec/flashmq/libflashmq-dbus-plugin.so 16 | expire_sessions_after_seconds 86400 17 | include_dir /run/user/${UID}/flashmq.d 18 | allow_anonymous true 19 | zero_byte_username_is_anonymous true 20 | log_level notice 21 | retained_messages_mode enabled_without_retaining 22 | 23 | _ 24 | 25 | if [ -v MQTT ] ; then 26 | cat >> /run/user/${UID}/flashmq.conf <<_ 27 | listen { 28 | protocol mqtt 29 | port ${MQTT} 30 | } 31 | _ 32 | fi 33 | 34 | if [ -v MQTTWS ] ; then 35 | cat >> /run/user/$UID/flashmq.conf <<_ 36 | listen { 37 | protocol websockets 38 | port ${MQTTWS} 39 | } 40 | _ 41 | fi 42 | 43 | -------------------------------------------------------------------------------- /lib/bin/get-unique-id: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | unset MACHID 5 | 6 | test -v USER || USER=$(id -un) 7 | F=/etc/venusian/$USER/vars 8 | test ! -f $F || . $F 9 | if test -v MACHID ; then 10 | echo $MACHID 11 | exit 0 12 | fi 13 | 14 | test -v UID || UID=$(id -u) 15 | F=/etc/venusian/vars 16 | test ! -f $F || . $F 17 | test -v MACHID || MACHID=$(head -13c /etc/machine-id) 18 | printf '%s%04x\n' $MACHID $UID 19 | -------------------------------------------------------------------------------- /lib/bin/iptables-redirect: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | usage() ( 5 | echo "Usage: $0 on|off" >&2 6 | exit 2 7 | ) 8 | if [ $# != 1 ] ; then usage; fi 9 | 10 | if [ "$1" = "on" ] ; then 11 | I=I 12 | elif [ "$1" = "off" ] ; then 13 | I=D 14 | else 15 | usage 16 | fi 17 | 18 | 19 | 20 | if [ -v SCREEN ] ; then 21 | P=$(( 5900 + $SCREEN )) 22 | [ -v MQTT ] || MQTT=$(( 51880 + $SCREEN )) 23 | [ -v MQTTWS ] || MQTTWS=$(( 59000 + $SCREEN )) 24 | 25 | # VNC: we want incoming packets to go to the server 26 | # even though the thing only listens on localhost 27 | for I in $(ls /sys/class/net) ; do 28 | if [ "$I" = "lo" ] ; then continue ; fi 29 | iptables -t nat -$I PREROUTING -i $I -p tcp --dport $P -j DNAT --to-destination 127.0.0.1:$P 30 | done 31 | fi 32 | 33 | # MQTT: Venusian clients need to talk to their own server, not the system's 34 | if [ -v MQTT ] ; then 35 | iptables -t nat -$I OUTPUT -o lo -p tcp -m owner --uid-owner $UID -m tcp --dport 1883 -j REDIRECT --to-ports $MQTT 36 | fi 37 | 38 | if [ -v MQTTWS ] ; then 39 | iptables -t nat -$I OUTPUT -o lo -p tcp -m owner --uid-owner $UID -m tcp --dport 9001 -j REDIRECT --to-ports $MQTTWS 40 | fi 41 | -------------------------------------------------------------------------------- /lib/bin/serial-add: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "$1" >>/tmp/v-serial-add 4 | echo "$1" | socat STDIN UNIX-SENDTO:/run/venusian/serial-add 5 | # We don't use a FIFO here because that blocks when nobody's listening 6 | # The receiver uses "socat UNIX-RECVFROM:/run/venusian/serial-add,fork STDOUT 7 | -------------------------------------------------------------------------------- /lib/bin/softlimit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEMP=$(getopt -o '+a:d:s:h' -n "$0" -- "$@") 4 | if [ $? -ne 0 ]; then 5 | echo "Error: args? $*" >&2 6 | exit 1 7 | fi 8 | 9 | set -e 10 | eval set -- "$TEMP" 11 | unset TEMP 12 | 13 | while : ; do 14 | case "$1" in 15 | (-h|--help) 16 | echo "Duh." 17 | exit 0 ;; 18 | (-d|-s|-a) 19 | shift 2 ;; 20 | (--) 21 | shift; break ;; 22 | (*) 23 | break ;; 24 | esac 25 | done 26 | 27 | exec "$@" 28 | -------------------------------------------------------------------------------- /lib/bin/start-gui: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec 2>&1 3 | 4 | echo "*** Starting gui ***" 5 | 6 | if [ "$FB" = "y" ] ; then 7 | # XXX untested and not preconfigured 8 | export QT_QPA_PLATFORM=linuxfb: 9 | size=720x480 10 | opts="-display VNC:size=$size:depth=32:passwordFile=/data/conf/vncpassword.txt:$SCREEN" 11 | else 12 | export QT_QPA_PLATFORM=vnc:size=480x300:addr=127.0.0.1:port=$(expr 5900 + $SCREEN) 13 | opts= 14 | fi 15 | export QT_PLUGIN_PATH=/v/u/lib/plugins,/usr/lib/venusian/opt/gui/gfxdrivers 16 | 17 | exec /opt/victronenergy/gui/gui $opts 18 | 19 | -------------------------------------------------------------------------------- /lib/bin/udev-handler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import sys 7 | import click 8 | import pyudev as udev 9 | import logging 10 | import time 11 | import re 12 | import pwd 13 | import anyio 14 | import socket 15 | import shlex 16 | from asyncdbus import MessageBus, Message, Variant 17 | from pprint import pprint 18 | from pathlib import Path 19 | from contextlib import suppress 20 | from fnmatch import fnmatch 21 | 22 | from yaml import safe_load as yload 23 | 24 | from bdb import BdbQuit as DebugException 25 | 26 | logger = logging.getLogger("handler") 27 | 28 | class BusError(RuntimeError): 29 | pass 30 | 31 | class NoSuchService(BusError): 32 | def __str__(self): 33 | return f"Service {self.args[0]} unknown" 34 | 35 | 36 | class unwrap: 37 | """ 38 | A sync+async context manager that unwraps single-element 39 | exception groups. 40 | """ 41 | def __call__(self): 42 | "Singleton. Returns itself." 43 | return self 44 | 45 | def __enter__(self): 46 | return self 47 | 48 | async def __aenter__(self): 49 | return self 50 | 51 | def __exit__(self, c, e, t): 52 | if e is None: 53 | return 54 | while isinstance(e, BaseExceptionGroup): 55 | if len(e.exceptions) == 1: 56 | e = e.exceptions[0] 57 | raise e from None 58 | 59 | async def __aexit__(self, c, e, t): 60 | return self.__exit__(c, e, t) 61 | 62 | unwrap = unwrap() 63 | 64 | 65 | @click.command 66 | @click.option("-c","--config",required=True,multiple=True,type=click.Path(file_okay=True,dir_okay=False,readable=True,exists=True), help="Config file (YAML)") 67 | @click.option("-u","--user",type=str, default="venus", help="User to target (default: venus)") 68 | @click.option("-v","--verbose",count=True, help="Be more verbose") 69 | def main(config, user, verbose): 70 | """ 71 | Process new Venus devices. 72 | 73 | This program reads /run/venusian and starts the appropriate service. 74 | 75 | More than one config file can be used. They are concatenated. 76 | The first matching record is used. 77 | 78 | The configuration has the form:: 79 | 80 | dbus: 81 | - match: 82 | key1: pattern 83 | key2: another_pattern 84 | service: NAME 85 | data: 86 | foo: bar 87 | baz: 420 88 | serial: @ID_SERIAL_SHORT 89 | user: venus 90 | 91 | Other keys are ignored. The user defaults to venus. 92 | A missing service skips this device; the next config file is used. 93 | service=null ignores this device. 94 | 95 | A non-NULL service name causes the service "NAME@DEVICE" to start. 96 | The unit file is located at /usr/lib/venusian/service/NAME@.service 97 | (but can be overridden using /var/lib/venusian/USER/service). 98 | 99 | Values from "data" are forwarded as environment variables: 100 | the value of "foo" is set as "VE_FOO". 101 | A data entry that starts with '@' refers to the named device property. 102 | """ 103 | logging.basicConfig(level=logging.WARNING if verbose == 0 else logging.INFO if verbose == 1 else logging.DEBUG) 104 | 105 | config = [ Path(f) for f in config ] 106 | 107 | h = Handler(config=config, user=user) 108 | try: 109 | anyio.run(h.run, backend="trio") 110 | except* KeyboardInterrupt: 111 | if verbose: 112 | raise 113 | print("Interrupted.", file=sys.stderr) 114 | 115 | 116 | class Handler: 117 | bad_serial = re.compile(r"(\w)\1{7,}\b") 118 | 119 | _intro_service = None 120 | _intro_job = None 121 | 122 | def __init__(self, config: list[Path], user: str|None = None): 123 | self.cfg_files = config 124 | self.read_cfg(first=True) 125 | if user is None: 126 | user = os.environ["USER"] 127 | self.uid = pwd.getpwnam(user).pw_uid 128 | self.user = user 129 | self.envdir = Path(f"/run/user/{self.uid}") 130 | self.envdir.mkdir(exist_ok=True) 131 | 132 | self.known = dict() 133 | self._reload_done = None 134 | self._reload_evt = anyio.Lock() 135 | 136 | async def run(self): 137 | bus = f"unix:path=/run/user/{self.uid}/bus" 138 | 139 | self._udev = context = udev.Context() 140 | try: 141 | async with ( 142 | unwrap, 143 | anyio.create_task_group() as self.tg, 144 | MessageBus(bus).connect() as self.dbus, 145 | ): 146 | await self._dbus_setup() 147 | 148 | await self._run() 149 | finally: 150 | with suppress(AttributeError): 151 | del self._udev 152 | del self.tg 153 | del self.dbus 154 | 155 | # async def reload(self): 156 | # "reload systemd" 157 | 158 | # if self._reload is not None: 159 | # # Currently reloading. Wait for it to finish. 160 | # await self._reload.wait() 161 | # if self._reload is not None: 162 | # # Another task was faster. Wait for its reload to finish. 163 | # await self._reload.wait() 164 | # return 165 | 166 | # self._reload = anyio.Event() 167 | # logger.error("Reload Start") 168 | # await self.db_mgr.call_reload() 169 | # logger.error("Reload Done") 170 | 171 | # self._reload.set() 172 | # self._reload = None 173 | 174 | def write_env(self,unit,env): 175 | "write a service's environment file." 176 | 177 | envf = self.envdir / unit 178 | with envf.open("w") as fd: 179 | for k,v in env.items(): 180 | print(f"{k}={shlex.quote(str(v))}", file=fd) 181 | 182 | async def _dbus_setup(self): 183 | self.intro = await self.dbus.introspect( 184 | 'org.freedesktop.systemd1', '/org/freedesktop/systemd1') 185 | self.db_root = await self.dbus.get_proxy_object( 186 | 'org.freedesktop.systemd1', '/org/freedesktop/systemd1', self.intro) 187 | self.db_mgr = await self.db_root.get_interface('org.freedesktop.systemd1.Manager') 188 | self.db_prop = await self.db_root.get_interface('org.freedesktop.DBus.Properties') 189 | 190 | # # for simplicity we assume these are present 191 | # self.intro_unit = await self.dbus.introspect( 192 | # 'org.freedesktop.systemd1.Unit', '/org/freedesktop/systemd1/unit/dbus_2eservice') 193 | # self.intro_service = await self.dbus.introspect( 194 | # 'org.freedesktop.systemd1.Service', '/org/freedesktop/systemd1/unit/dbus_2eservice') 195 | 196 | async def _run(self): 197 | monitor = udev.Monitor.from_netlink(self._udev) 198 | monitor.filter_by(subsystem="tty") # others? 199 | 200 | logger.debug("processing existing devices") 201 | for dev in self._udev.list_devices(subsystem="tty",ID_BUS="usb",SUBSYSTEMS="platform|usb-serial"): 202 | await self.process(dev) 203 | logger.debug("waiting for changes") 204 | 205 | n_empty = 0 206 | fd = socket.fromfd(monitor.fileno(), socket.AF_NETLINK, socket.SOCK_STREAM) 207 | while True: 208 | await anyio.wait_socket_readable(fd) 209 | dev = monitor.poll(timeout=0) 210 | if dev is None: 211 | if n_empty > 3: 212 | raise RuntimeError("No bus data") 213 | logger.debug("empty") 214 | n_empty += 1 215 | continue 216 | 217 | n_empty = 0 218 | name = dev.sys_name 219 | if dev.action == "add": 220 | await self.process(dev) 221 | elif dev.action == "remove": 222 | await self.stop(name) 223 | else: 224 | logger.warning("Unknown action %r for %r", dev.action, name) 225 | 226 | def find_cfg(self, dev): 227 | prop = dev.properties 228 | def match1(rule: dict[str,str]) -> bool: 229 | for k,v in rule.items(): 230 | try: 231 | r = prop[k] 232 | except KeyError: 233 | try: 234 | r = getattr(dev,k) 235 | except AttributeError: 236 | r = None 237 | if v is None: 238 | if r is not None: 239 | return False 240 | else: 241 | try: 242 | if not fnmatch(r, v): 243 | return False 244 | except KeyError: 245 | return False 246 | return True 247 | 248 | def match2(rule: dict): 249 | try: 250 | rules = rule["match"] 251 | except KeyError: 252 | return None 253 | if isinstance(rules, list): 254 | for r in rules: 255 | if match1(r): 256 | return True 257 | return False 258 | else: 259 | return match1(rules) 260 | 261 | for cfg in self.cfg: 262 | cfg = cfg.get("udev", None) 263 | if not cfg: 264 | continue 265 | for rule in cfg: 266 | m = match2(rule) 267 | if not m: 268 | continue 269 | s = rule.get("service", False) 270 | if s is None: 271 | return None 272 | elif s is False: 273 | break 274 | if (user := rule.get("user", self.user)) != self.user: 275 | logger.debug("Not our device: %r: %s for %s", dev.sys_name, service, user) 276 | return None 277 | return rule 278 | 279 | return None 280 | 281 | 282 | async def process(self, dev): 283 | name = dev.sys_name 284 | service = self.find_cfg(dev) 285 | if service is None: 286 | logger.warning("Unknown device: %r.", name) 287 | logger.warning(" possible 'match' rules:\n%s", self.recommend_match(dev)) 288 | srv = self.recommend_service(dev) or "modbus # this is a wild guess" 289 | logger.warning(" possible service: %s", srv) 290 | 291 | for k,v in dev.properties.items(): 292 | logger.info(" %s=%r", k,v) 293 | return 294 | logger.info("Known device: %r: %s", name,service["service"]) 295 | try: 296 | with unwrap: 297 | await self.start(name,service,dev) 298 | except DebugException: 299 | raise 300 | except BusError as exc: 301 | logger.error("Unable to start: %s", exc) 302 | except Exception as exc: 303 | logger.exception("Unable to start %s@%s: %r", service["service"], name, exc) 304 | 305 | def recommend_match(self, dev): 306 | "return a text with match suggestions" 307 | p = dev.properties 308 | ser = p.get("ID_USB_SERIAL_SHORT") 309 | if ser and len(ser) > 3 and not self.bad_serial.match(ser) and "234567" not in ser and "bcdefg" not in ser.lower(): 310 | # ignore obviously-fake or broken serial numbers 311 | return f"ID_USB_SERIAL: {p['ID_USB_SERIAL']}" 312 | path = p.get("ID_PATH_TAG") 313 | if path is not None: 314 | return f"""\ 315 | ID_USB_VENDOR_ID: {p.get('ID_USB_VENDOR_ID') !r} 316 | ID_USB_MODEL_ID: {p.get('ID_USB_MODEL_ID') !r} 317 | ID_PATH_TAG: {path} # you might want to add wildcards""" 318 | 319 | return f"""\ 320 | ID_USB_VENDOR_ID: {p.get('ID_USB_VENDOR_ID') !r} 321 | ID_USB_MODEL_ID: {p.get('ID_USB_MODEL_ID') !r} 322 | sys_name: {dev.sys_name} # Try to avoid this! 323 | # run "udevadm info {P.get('DEVNAME')}" for more options""" 324 | 325 | def recommend_service(self, dev): 326 | p = dev.properties 327 | ve = p.get("VE_SERVICE") 328 | if ve is not None: 329 | return ve 330 | 331 | async def service_for(self, uname): 332 | unit = await self.db_mgr.call_load_unit(uname) 333 | 334 | if self._intro_service is None: 335 | self._intro_service = await self.dbus.introspect('org.freedesktop.systemd1',unit) 336 | proxy = await self.dbus.get_proxy_object('org.freedesktop.systemd1', unit, self._intro_service) 337 | 338 | s = await proxy.get_interface('org.freedesktop.systemd1.Service') 339 | u = await proxy.get_interface('org.freedesktop.systemd1.Unit') 340 | # p = await proxy.get_interface('org.freedesktop.DBus.Properties') 341 | st = await u.get_load_state() 342 | if st == "not-found": 343 | raise NoSuchService(uname) 344 | return s,u #,p 345 | 346 | async def start(self, name, service, dev, retry=False): 347 | if name in self.known: 348 | logger.error("KNOWN %s %s",name, service) 349 | return 350 | sname = service["service"] 351 | self.known[name] = service 352 | logger.info("START %s %s",sname,name) 353 | suname = f"{sname}@{name}" 354 | uname = f"{suname}.service" 355 | 356 | env = {} 357 | for k,v in service.get("data",{}).items(): 358 | if isinstance(v,str) and v.startswith("@"): 359 | v = v[1:] 360 | try: 361 | v = dev.properties[v] 362 | except KeyError: 363 | v = getattr(dev, v) 364 | k = k.upper() 365 | env[k] = v 366 | 367 | s,u = await self.service_for(uname) 368 | self.write_env(suname,env) 369 | 370 | job = await u.call_start("fail") 371 | try: 372 | if self._intro_job is None: 373 | self._intro_job = await self.dbus.introspect('org.freedesktop.systemd1',job) 374 | jproxy = await self.dbus.get_proxy_object('org.freedesktop.systemd1', job, self._intro_job) 375 | j = await jproxy.get_interface('org.freedesktop.systemd1.Job') 376 | js = await j.get_state() 377 | except DebugException: 378 | raise 379 | except Exception as exc: 380 | logger.error("Job %s: %r", job, exc) 381 | else: 382 | logger.error("Job %s: %s", job, js) 383 | 384 | sr = await s.get_result() 385 | if sr != "success": 386 | logger.error("Start %s: Job %r: %s", sname, job, sr) 387 | 388 | 389 | async def stop(self, name): 390 | try: 391 | service = self.known.pop(name) 392 | except KeyError: 393 | return 394 | sname = service["service"] 395 | logger.info("STOP %s %s",sname,name) 396 | uname = f"{sname}@{name}.service" 397 | 398 | s,u = await self.service_for(uname) 399 | job = await u.call_stop("fail") 400 | 401 | sr = await s.get_result() 402 | if sr != "success": 403 | logger.error("Stop %s: Job %r: %s", sname, job, sr) 404 | 405 | 406 | def read_cfg(self, first:bool = False): 407 | cfg = [{}] * len(self.cfg_files) if first else self.cfg 408 | 409 | for n,(c,fn) in enumerate(zip(cfg, self.cfg_files)): 410 | ts = fn.stat().st_ctime 411 | if c.get("_t",0) == ts: 412 | continue 413 | 414 | with fn.open("r") as f: 415 | try: 416 | with unwrap: 417 | cf = yload(f) 418 | if not isinstance(cf, dict): 419 | raise RuntimeError(f"YAML {fn !r} is not a mapping.") 420 | except DebugException: 421 | raise 422 | except Exception: 423 | if prev is None: 424 | raise # first run must work 425 | logger.exception("Reading %r", fn) 426 | continue 427 | 428 | cf["_t"] = ts 429 | cfg[n] = cf 430 | 431 | self.cfg = cfg 432 | 433 | 434 | if __name__ == "__main__": 435 | try: 436 | with unwrap: 437 | main() 438 | except (DebugException,KeyboardInterrupt): 439 | print("Interrupted.", file=sys.stderr) 440 | sys.exit(1) 441 | -------------------------------------------------------------------------------- /lib/bin/vbus: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | U=venus 4 | if test $# -gt 2 && test "$1" = "-u" ; then U=$2; shift 2; fi 5 | 6 | exec sudo -u $U env DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u venus)/bus /opt/victronenergy/usr/bin/dbus "$@" 7 | -------------------------------------------------------------------------------- /lib/bin/vctl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | U=venus 4 | if test $# -gt 2 && test "$1" = "-u" ; then U=$2; shift 2; fi 5 | 6 | exec sudo -u $U env DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u venus)/bus systemd-run --user --scope systemctl --user "$@" 7 | -------------------------------------------------------------------------------- /lib/bin/ven: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | U=venus 4 | if test $# -gt 2 && test "$1" = "-u" ; then U=$2; shift 2; fi 5 | 6 | if test $# = 0 ; then set -- /bin/bash; fi 7 | exec sudo -u $U env DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u venus)/bus systemd-run --user --scope "$@" 8 | -------------------------------------------------------------------------------- /patch/dbus-modbus-client/client.py: -------------------------------------------------------------------------------- 1 | --- /opt/venus/opt/victronenergy/dbus-modbus-client/client.py 2025-02-10 22:25:03.000000000 +0100 2 | +++ /tmp/client.py 2025-02-24 10:40:00.815857087 +0100 3 | @@ -3,10 +3,16 @@ 4 | import threading 5 | import time 6 | 7 | -from pymodbus.client.sync import * 8 | -from pymodbus.utilities import computeCRC 9 | +from pymodbus.client import * 10 | +try: 11 | + from pymodbus.utilities import computeCRC 12 | +except ImportError: 13 | + from pymodbus.message.rtu import MessageRTU 14 | + computeCRC = MessageRTU.compute_CRC 15 | +from pymodbus.framer.rtu_framer import ModbusRtuFramer 16 | +from pymodbus.framer.ascii_framer import ModbusAsciiFramer 17 | 18 | -class ModbusExtras: 19 | +class RefCount: 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | self.refcount = 1 23 | @@ -33,19 +39,10 @@ 24 | finally: 25 | self.in_transaction = False 26 | 27 | - def read_registers(self, address, count, access, **kwargs): 28 | - if access == 'holding': 29 | - return self.read_holding_registers(address, count, **kwargs) 30 | - 31 | - if access == 'input': 32 | - return self.read_input_registers(address, count, **kwargs) 33 | - 34 | - raise Exception('Invalid register access type: %s' % access) 35 | - 36 | -class TcpClient(ModbusExtras, ModbusTcpClient): 37 | +class TcpClient(RefCount, ModbusTcpClient): 38 | method = 'tcp' 39 | 40 | -class UdpClient(ModbusExtras, ModbusUdpClient): 41 | +class UdpClient(RefCount, ModbusUdpClient): 42 | method = 'udp' 43 | 44 | @property 45 | @@ -58,25 +55,30 @@ 46 | if self.socket: 47 | self.socket.settimeout(t) 48 | 49 | -class SerialClient(ModbusExtras, ModbusSerialClient): 50 | - def __init__(self, *args, **kwargs): 51 | - super().__init__(*args, **kwargs) 52 | +class SerialClient(RefCount, ModbusSerialClient): 53 | + def __init__(self, *args, method = None, **kwargs): 54 | + if method == "rtu": 55 | + framer = ModbusRtuFramer 56 | + elif method == "ascii": 57 | + framer = ModbusAsciiFramer 58 | + else: 59 | + raise ValueError("RTU or ASCII only") 60 | + self.method = method 61 | + super().__init__(*args, framer=framer, **kwargs) 62 | self.lock = threading.RLock() 63 | 64 | @property 65 | def timeout(self): 66 | - return self._timeout 67 | + return self.params.timeout 68 | 69 | @timeout.setter 70 | def timeout(self, t): 71 | - self._timeout = t 72 | - if self.socket: 73 | - self.socket.timeout = t 74 | + self.params.timeout = t 75 | 76 | def put(self): 77 | super().put() 78 | if self.refcount == 0: 79 | - del serial_ports[os.path.basename(self.port)] 80 | + del serial_ports[os.path.basename(self.params.port)] 81 | 82 | def execute(self, request=None): 83 | with self.lock: 84 | @@ -108,7 +110,7 @@ 85 | return client.get() 86 | 87 | dev = '/dev/%s' % tty 88 | - client = SerialClient(m.method, port=dev, baudrate=m.rate) 89 | + client = SerialClient(port=dev, baudrate=m.rate, method=m.method) 90 | if not client.connect(): 91 | client.put() 92 | return None 93 | -------------------------------------------------------------------------------- /patch/dbus-modbus-client/dbus-modbus-client.py: -------------------------------------------------------------------------------- 1 | --- /opt/venus/opt/victronenergy/dbus-modbus-client/dbus-modbus-client.py 2025-02-10 22:25:03.000000000 +0100 2 | +++ /tmp/dbus-modbus-client.py 2025-02-24 10:58:29.332691897 +0100 3 | @@ -36,14 +36,30 @@ 4 | import victron_em 5 | 6 | import logging 7 | -log = logging.getLogger() 8 | +log = logging.getLogger("d-modbus-client") 9 | + 10 | +sys.path.insert(1,sys.path[0]+"/meter-library") 11 | +try: 12 | + import ABB_B2x 13 | + import EM24RTU 14 | + import Eastron_SDM120 15 | + import Eastron_SDM630v1 16 | + import Eastron_SDM630v2 17 | + import Eastron_SDM72D 18 | +except ImportError: 19 | + pass 20 | 21 | NAME = os.path.basename(__file__) 22 | VERSION = '1.62' 23 | 24 | __all__ = ['NAME', 'VERSION'] 25 | 26 | -pymodbus.constants.Defaults.Timeout = 0.5 27 | +try: 28 | + pymodbus.constants.Defaults.Timeout = 0.5 29 | +except AttributeError: 30 | + timeout_arg = {"timeout":0.5} 31 | +else: 32 | + timeout_arg = {} 33 | 34 | MODBUS_TCP_PORT = 502 35 | 36 | @@ -424,9 +440,10 @@ 37 | 38 | if args.serial: 39 | tty = os.path.basename(args.serial) 40 | - client = SerialClient(tty, args.rate, args.mode) 41 | + client = SerialClient(tty, args.rate, args.mode, **timeout_arg) 42 | else: 43 | client = NetClient() 44 | + # client = NetClient(**timeout_arg) 45 | 46 | client.err_exit = args.exit 47 | client.init(args.force_scan) 48 | -------------------------------------------------------------------------------- /patch/dbus-modbus-client/dbus-modbus-client.py.V/v3.54: -------------------------------------------------------------------------------- 1 | --- /opt/venus/opt/victronenergy/dbus-modbus-client/dbus-modbus-client.py 2025-01-28 14:16:07.000000000 +0100 2 | +++ /tmp/dbus-modbus-client.py 2025-03-15 15:33:11.781433157 +0100 3 | @@ -36,14 +36,30 @@ 4 | import victron_em 5 | 6 | import logging 7 | -log = logging.getLogger() 8 | +log = logging.getLogger("d-modbus-client") 9 | + 10 | +sys.path.insert(1,sys.path[0]+"/meter-library") 11 | +try: 12 | + import ABB_B2x 13 | + import EM24RTU 14 | + import Eastron_SDM120 15 | + import Eastron_SDM630v1 16 | + import Eastron_SDM630v2 17 | + import Eastron_SDM72D 18 | +except ImportError: 19 | + pass 20 | 21 | NAME = os.path.basename(__file__) 22 | VERSION = '1.61' 23 | 24 | __all__ = ['NAME', 'VERSION'] 25 | 26 | -pymodbus.constants.Defaults.Timeout = 0.5 27 | +try: 28 | + pymodbus.constants.Defaults.Timeout = 0.5 29 | +except AttributeError: 30 | + timeout_arg = {"timeout":0.5} 31 | +else: 32 | + timeout_arg = {} 33 | 34 | MODBUS_TCP_PORT = 502 35 | 36 | @@ -424,9 +440,10 @@ 37 | 38 | if args.serial: 39 | tty = os.path.basename(args.serial) 40 | - client = SerialClient(tty, args.rate, args.mode) 41 | + client = SerialClient(tty, args.rate, args.mode, **timeout_arg) 42 | else: 43 | client = NetClient() 44 | + # client = NetClient(**timeout_arg) 45 | 46 | client.err_exit = args.exit 47 | client.init(args.force_scan) 48 | -------------------------------------------------------------------------------- /patch/dbus-mqtt/dbus_mqtt.py: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -e 's/retain=True/retain=False/' 4 | -------------------------------------------------------------------------------- /patch/dbus-mqtt/mqtt_gobject_bridge.py: -------------------------------------------------------------------------------- 1 | --- /opt/victronenergy/dbus-mqtt/mqtt_gobject_bridge.py 2025-02-10 22:36:17.000000000 +0100 2 | +++ /tmp/mqtt_gobject_bridge.py 2025-02-24 10:31:45.802900668 +0100 3 | @@ -22,7 +22,7 @@ 4 | self._mqtt_user = user 5 | self._mqtt_passwd = passwd 6 | self._mqtt_server = mqtt_server or '127.0.0.1' 7 | - self._client = paho.mqtt.client.Client(client_id) 8 | + self._client = paho.mqtt.client.Client(paho.mqtt.client.CallbackAPIVersion.VERSION1, client_id) 9 | self._client.on_connect = self._on_connect 10 | self._client.on_message = self._on_message 11 | self._client.on_disconnect = self._on_disconnect 12 | -------------------------------------------------------------------------------- /patch/dbus-shelly/dbus_shelly.py: -------------------------------------------------------------------------------- 1 | --- /opt/venus/opt/victronenergy/dbus-shelly/dbus_shelly.py 2025-02-10 22:25:03.000000000 +0100 2 | +++ /opt/victronenergy/dbus-shelly/dbus_shelly.py 2025-02-24 15:33:09.231998042 +0100 3 | @@ -96,15 +96,11 @@ 4 | "session": BusType.SESSION 5 | }.get(args.dbus, BusType.SESSION) 6 | 7 | - mainloop = asyncio.get_event_loop() 8 | - mainloop.run_until_complete( 9 | - websockets.serve(Server(lambda: Meter(bus_type)), '', 8000)) 10 | - 11 | - try: 12 | - logger.info("Starting main loop") 13 | - mainloop.run_forever() 14 | - except KeyboardInterrupt: 15 | - mainloop.stop() 16 | + logger.info("Starting main loop") 17 | + async def _main(): 18 | + await websockets.serve(Server(lambda: Meter(bus_type)), '', 8000+int(os.environ.get("SCREEN",1))) 19 | + await asyncio.Future() 20 | + asyncio.run(_main()) 21 | 22 | 23 | if __name__ == "__main__": 24 | -------------------------------------------------------------------------------- /patch/dbus-systemcalc-py/sc_utils.py: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sed -e 's/from collections import/from collections.abc import/' 3 | -------------------------------------------------------------------------------- /patch/mk2-dbus/start-mk2-dbus.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -e 's#^start #exec $app --dbus session #' -e 's/dbus-send --system/dbus-send --session/' 4 | -------------------------------------------------------------------------------- /patch/vedirect-interface/start-vedirect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -e 's#^start #exec $app --dbus session #' 4 | -------------------------------------------------------------------------------- /service/flashmq.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Per-User FlashMQ MQTT server 3 | ConditionEnvironment=MQTT 4 | 5 | [Service] 6 | Type=simple 7 | LimitNOFILE=65536 8 | ExecStartPre=+/bin/mkdir -p /run/user/%U/flashmq_include 9 | ExecStartPre=+/bin/chown venus:venus /run/user/%U/flashmq_include 10 | ExecStartPre=/usr/lib/venusian/bin/gen-flashmq-conf /run/user/%U/flashmq.conf 11 | ExecStart=/usr/bin/flashmq --config-file /run/user/%U/flashmq.conf 12 | ExecReload=/bin/kill -HUP $MAINPID 13 | Restart=on-failure 14 | RestartSec=5s 15 | 16 | EnvironmentFile=-/etc/venusian/vars 17 | EnvironmentFile=-/etc/venusian/%u/%N 18 | EnvironmentFile=-/run/user/%U/vars 19 | EnvironmentFile=-/run/user/%U/%N 20 | 21 | [Install] 22 | WantedBy=multi-user.target 23 | -------------------------------------------------------------------------------- /service/mk3@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Serve an MK3 interface on %i 3 | After=gui.service localsettings.service 4 | Requires=localsettings.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/opt/victronenergy/mk2-dbus/start-mk2-dbus.sh %i PRODUCT=MK3-USB_Interface 9 | Restart=always 10 | RestartSec=5 11 | EnvironmentFile=-/etc/venusian/vars 12 | EnvironmentFile=-/etc/venusian/%u/vars 13 | EnvironmentFile=-/etc/venusian/%u/%p 14 | EnvironmentFile=-/etc/venusian/%u/%N 15 | EnvironmentFile=-/run/user/%U/vars 16 | EnvironmentFile=-/run/user/%U/%p 17 | EnvironmentFile=-/run/user/%U/%N 18 | -------------------------------------------------------------------------------- /service/modbus@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Serve an MK3 interface on %i 3 | After=gui.service localsettings.service 4 | Requires=localsettings.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/opt/victronenergy/dbus-modbus-client/dbus-modbus-client.py -s %i -r ${RATE} -m ${MODE} 9 | Restart=always 10 | RestartSec=5 11 | Environment=MODE=rtu 12 | EnvironmentFile=-/etc/venusian/vars 13 | EnvironmentFile=-/etc/venusian/%u/vars 14 | EnvironmentFile=-/etc/venusian/%u/%p 15 | EnvironmentFile=-/etc/venusian/%u/%N 16 | EnvironmentFile=-/run/user/%U/vars 17 | EnvironmentFile=-/run/user/%U/%p 18 | EnvironmentFile=-/run/user/%U/%N 19 | -------------------------------------------------------------------------------- /service/udev-handler.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=DBUS Starter 3 | After=localsettings.service 4 | Requires=localsettings.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=@USRV@/bin/udev-handler -u %u -c @LIBVV@/udev.yml -c @USRV@/udev.yml 9 | Restart=always 10 | RestartSec=15 11 | EnvironmentFile=-/etc/venusian/vars 12 | EnvironmentFile=-/etc/venusian/%u/%N 13 | EnvironmentFile=-/run/user/%U/vars 14 | EnvironmentFile=-/run/user/%U/%N 15 | -------------------------------------------------------------------------------- /service/vedirect@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Serve a VE-Direct interface on %i 3 | After=gui.service localsettings.service 4 | Requires=localsettings.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/opt/victronenergy/vedirect-interface/start-vedirect.sh %i PRODUCT=VE_Direct_cable 9 | Restart=always 10 | RestartSec=5 11 | EnvironmentFile=-/etc/venusian/vars 12 | EnvironmentFile=-/etc/venusian/%u/%p 13 | EnvironmentFile=-/etc/venusian/%u/%N 14 | EnvironmentFile=-/run/user/%U/vars 15 | EnvironmentFile=-/run/user/%U/%p 16 | EnvironmentFile=-/run/user/%U/%N 17 | -------------------------------------------------------------------------------- /udev.example.yml: -------------------------------------------------------------------------------- 1 | # This file adds to the entries in /usr/lib/venusian/udev.yml 2 | # 3 | # See there for documentation. 4 | # 5 | udev: 6 | # Example: 7 | # - match: 8 | # ID_USB_SERIAL: Silicon_Labs_CP2104_USB_to_UART_Bridge_Controller_00897C7A 9 | # service: modbus 10 | # data: 11 | # rate: 9600 12 | # mode: rtu 13 | -------------------------------------------------------------------------------- /udev.yml: -------------------------------------------------------------------------------- 1 | # This is the default Venusian udev file. 2 | # 3 | # DO NOT EDIT THIS FILE 4 | # 5 | # You can add your own by editing /var/lib/venusian/udev.yml 6 | # 7 | udev: 8 | # The following entries are a transcription of Victron's 9 | # "serial-starter.rules" udev file. 10 | # 11 | # Note that Venusian does not probe. You need to add actual 12 | # services, and whatever other parameters your service needs 13 | # (like the baud rate), to /var/lib/venusian/udev.yml. 14 | # 15 | # Supported services and accepted data: 16 | # 17 | # mk3: the MK3 USB Interface 18 | # 19 | # vedirect: the official VE-Direct adapter 20 | # 21 | # modbus: 22 | # rate: baud rate (9600, 19200, …). Must be specified. 23 | # mode: rtu (rtu, ascii). The default is "rtu". 24 | # 25 | - match: 26 | ID_BUS: usb 27 | ID_MODEL: FT232R_USB_UART 28 | service: null # rs485 29 | 30 | - match: 31 | ID_BUS: usb 32 | ID_MODEL: USB-RS485_Cable 33 | service: null # rs485 34 | 35 | - match: 36 | ID_BUS: usb 37 | ID_MODEL: USB485_Iso_stick 38 | service: null # rs485 39 | 40 | - match: 41 | ID_BUS: usb 42 | ID_MODEL: USB-Serial_Controller_D 43 | service: null # cgwacs 44 | 45 | - match: 46 | ID_BUS: usb 47 | ID_MODEL: CP2102_USB_to_UART_Bridge_Controller 48 | service: null # cgwacs 49 | 50 | - match: 51 | ID_BUS: usb 52 | ID_VENDOR_ID: "1a86" 53 | ID_MODEL_ID: "7523" 54 | service: null # cgwacs 55 | 56 | - match: 57 | ID_BUS: usb 58 | ID_MODEL: VE_Direct_cable 59 | service: vedirect 60 | 61 | - match: 62 | ID_BUS: usb 63 | ID_MODEL: FT232EX 64 | service: null # vedirect 65 | 66 | - match: 67 | ID_BUS: usb 68 | ID_MODEL: MK3-USB_Interface 69 | service: mk3 # originally "mkx" 70 | --------------------------------------------------------------------------------