├── .formatter.exs ├── .gitignore ├── .tool-versions ├── Makefile ├── README.md ├── config ├── config.exs ├── host.exs └── target.exs ├── custom_boot ├── config.txt └── fwup.conf ├── lib ├── xdoor.ex └── xdoor │ ├── application.ex │ ├── authorized_keys.ex │ ├── lock_control.ex │ ├── lock_state.ex │ ├── monitor.ex │ ├── motion_detection.ex │ ├── ssh_keys.ex │ └── ssh_server.ex ├── mix.exs ├── mix.lock ├── pic1.jpg ├── pic2.jpg ├── pic3.jpg ├── priv ├── .gitignore └── greeting ├── rel └── vm.args.eex ├── rootfs_overlay └── etc │ └── iex.exs ├── ssh_console.sh └── test ├── test_helper.exs └── xdoor_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 120 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | .elixir_ls 19 | 20 | /secrets 21 | /storage 22 | priv/authorized_keys/authorized_keys 23 | /upload.sh 24 | 25 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 24.0.5 2 | elixir ref:v1.12.2 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export MIX_ENV = prod 2 | export MIX_TARGET = rpi3 3 | 4 | 5 | burn-complete: 6 | mix firmware ;\ 7 | mix firmware.burn --task complete 8 | 9 | burn-upgrade: 10 | mix firmware ;\ 11 | mix firmware.burn --task upgrade 12 | 13 | push: 14 | mix firmware &&\ 15 | rm -f upload.sh &&\ 16 | mix firmware.gen.script &&\ 17 | SSH_OPTIONS="-p 23" ./upload.sh xdoor.lan.xhain.space 18 | 19 | deps-get: 20 | mix local.hex --force ;\ 21 | mix local.rebar --force ;\ 22 | mix deps.get 23 | 24 | deps-update: 25 | mix deps.update --all 26 | 27 | shell: 28 | ./ssh_console.sh xdoor.lan.xhain.space 29 | 30 | console: 31 | MIX_TARGET=host MIX_ENV=dev iex -S mix 32 | 33 | clean: 34 | mix clean 35 | mix nerves.clean --all 36 | mix deps.clean --all 37 | 38 | logs: 39 | ssh admin@xdoor logs 40 | 41 | lock-state-changes: 42 | ssh admin@xdoor lock_state_changes 43 | 44 | logins: 45 | ssh admin@xdoor logins 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xDoor 2 | 3 | ## Description 4 | 5 | Operates the door of the xHain using ssh as authentication. After successful ssh login (`open@xdoor` or `close@xdoor`) the lock motor is triggered via gpio. 6 | 7 | It is build in elixir using the [nerves-framework](https://hexdocs.pm/nerves/getting-started.html) and runs on any Rapsberry Pi. 8 | 9 | ## Authorized Keys 10 | 11 | The authentication is done via an `authorized_keys` file. This file is regularly pulled from a server to allow remote updates. The signature of that file is verified against the public key in `priv/authorized_keys_pub.pem`. See `lib/xdoor/authorized_keys.ex` for implementation details. 12 | 13 | # Building 14 | 15 | Elixir `1.9` or greater is required to build and flash the xdoor firmware. This repo contains a `.tool-versions` for [asdf](https://asdf-vm.com). Running 16 | ``` 17 | asdf install 18 | ``` 19 | should install everything required. 20 | 21 | ## SSH host_key 22 | 23 | The host_key for the ssh server is expected to lie in `priv/host_key/`. It can be generated with 24 | ``` 25 | ssh-keygen -t ed25519 -f ./priv/host_key/ssh_host_ed25519_key 26 | ``` 27 | Beware that regenerating this will prompt all clients to re-authenticate the fingerprint of the host. 28 | 29 | ## Makefile 30 | 31 | There are make target for all relevant operations. The most important ones are 32 | 33 | * `make deps-get burn-complete`: Get all dependencies, build the firmware image and flash to inserted sd-card. It tries to auto-detect the sd-card and asks for confirmation before flashing. 34 | * `make push`: Build firmware and update existing system via ssh. The `authorized_keys` for this are defined in `config/target.exs`. 35 | * `make console`: open an iex console prompt on the running system for debugging. 36 | 37 | 38 | # Hardware 39 | 40 | ## Used hardware for the locking mechanism 41 | * Equiva Doorlock 42 | 43 | 44 | * solder 2 cables (yellow and white) to the buttons, yellow is close, white is open 45 | 46 | 47 | * solder 2 cables (red and black) to connect the power-supply, if you don't want to rely on batteries. ST6 is GND, ST5 is battery voltage. There's a voltage regulator on the board, so a resistor is not necessary to get from 5V down to 4.5V. 48 | 49 | 50 | * Cut this trace to disable the bluetooth chip by disconnecting power. This way no one has to connect 8 fake profiles to the door. 51 | 52 | 53 | * drill a hole into the case for the cables 54 | 55 | 56 | * Locking cylinder 57 | * Standard cylinder - important: needs to be lockable with keys on both sides in the cylinder 58 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :xdoor, target: Mix.target() 4 | 5 | # Customize non-Elixir parts of the firmware. See 6 | # https://hexdocs.pm/nerves/advanced-configuration.html for details. 7 | 8 | config :nerves, :firmware, rootfs_overlay: "rootfs_overlay" 9 | 10 | # Set the SOURCE_DATE_EPOCH date for reproducible builds. 11 | # See https://reproducible-builds.org/docs/source-date-epoch/ for more information 12 | 13 | config :nerves, source_date_epoch: "1602059153" 14 | 15 | config :logger, 16 | backends: [:console, RingLogger], 17 | level: :debug 18 | 19 | config :ring_logger, format: "$time $metadata[$level]$levelpad $message\n" 20 | 21 | config :nerves_leds, names: [green: "led0", red: "led1"] 22 | 23 | if Mix.target() == :host or Mix.target() == :"" do 24 | import_config "host.exs" 25 | else 26 | import_config "target.exs" 27 | end 28 | -------------------------------------------------------------------------------- /config/host.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Add configuration that is only needed when running on the host here. 4 | 5 | config :xdoor, 6 | storage_dir: "./storage", 7 | ssh_port: 8022, 8 | authorized_keys_update_interval_ms: 20 * 1000, 9 | gpio_enabled: false 10 | 11 | config :nerves_ssh, 12 | authorized_keys: [ 13 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFk68ujMEgPVglDNnxqrht/0piGwofQy4GmPjgq4CvUV" 14 | ] 15 | -------------------------------------------------------------------------------- /config/target.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :xdoor, 4 | storage_dir: "/data/xdoor", 5 | ssh_port: 22, 6 | authorized_keys_update_interval_ms: 5 * 60 * 1000, 7 | gpio_enabled: true, 8 | enable_monitor: true 9 | 10 | config :shoehorn, 11 | init: [:nerves_runtime, :nerves_pack], 12 | app: Mix.Project.config()[:app] 13 | 14 | config :nerves_runtime, :kernel, use_system_registry: false 15 | 16 | config :nerves, 17 | erlinit: [ 18 | hostname_pattern: "xdoor" 19 | ] 20 | 21 | keys = [ 22 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFk68ujMEgPVglDNnxqrht/0piGwofQy4GmPjgq4CvUV" 23 | ] 24 | 25 | if keys == [], 26 | do: 27 | Mix.raise(""" 28 | No SSH public keys found in ~/.ssh. An ssh authorized key is needed to 29 | log into the Nerves device and update firmware on it using ssh. 30 | See your project's config.exs for this error message. 31 | """) 32 | 33 | config :nerves_ssh, 34 | port: 23, 35 | authorized_keys: keys 36 | 37 | config :vintage_net, 38 | regulatory_domain: "DE", 39 | config: [ 40 | {"eth0", 41 | %{ 42 | type: VintageNetEthernet, 43 | ipv4: %{method: :dhcp} 44 | }} 45 | ] 46 | 47 | config :nerves, :firmware, fwup_conf: "custom_boot/fwup.conf" 48 | 49 | config :mdns_lite, 50 | # The `host` key specifies what hostnames mdns_lite advertises. `:hostname` 51 | # advertises the device's hostname.local. For the official Nerves systems, this 52 | # is "nerves-<4 digit serial#>.local". mdns_lite also advertises 53 | # "nerves.local" for convenience. If more than one Nerves device is on the 54 | # network, delete "nerves" from the list. 55 | hosts: ["xdoor"], 56 | ttl: 120 57 | 58 | config :logger, level: :info 59 | -------------------------------------------------------------------------------- /custom_boot/config.txt: -------------------------------------------------------------------------------- 1 | # Default Nerves RPi 3 config.txt 2 | # 3 | # It's possible to override this file by using a custom fwup.conf 4 | # configuration to pull in a replacement. 5 | # 6 | # Useful links: 7 | # https://www.raspberrypi.org/documentation/configuration/config-txt/README.md 8 | # https://www.raspberrypi.org/documentation/configuration/device-tree.md 9 | # https://github.com/raspberrypi/documentation/blob/master/configuration/device-tree.md 10 | # https://github.com/raspberrypi/firmware/blob/master/boot/overlays/README 11 | 12 | kernel=zImage 13 | 14 | # Disable the boot rainbow 15 | disable_splash=1 16 | 17 | # This, along with the Raspberry Pi "x" firmware is needed for the camera 18 | # to work. The Raspberry Pi "x" firmware is selected via the Buildroot 19 | # configuration. See Target packages->Hardware handling->Firmware. 20 | gpu_mem=192 21 | 22 | # Disable I2C, SPI, and audio 23 | dtparam=i2c_arm=off 24 | dtparam=spi=off 25 | dtparam=audio=off 26 | 27 | # Enable drivers for the Raspberry Pi 7" Touchscreen 28 | # 29 | # This makes it possible to run Scenic out of the box with the popular 30 | # Raspberry Pi 7" Touchscreen. It appears to be harmless for non-touchscreen 31 | # users, but if not, let us know! 32 | dtoverlay=rpi-ft5406 33 | dtoverlay=rpi-backlight 34 | 35 | # Comment this in or modify to enable OneWire 36 | # NOTE: check that the overlay that you specify is in the boot partition or 37 | # this won't work. 38 | #dtoverlay=w1-gpio-pullup,gpiopin=4 39 | 40 | # The ramoops overlay works with the pstore driver to preserve crash 41 | # information across reboots in DRAM 42 | dtoverlay=ramoops 43 | 44 | # Enable the UART (/dev/ttyAMA0) on the RPi3. 45 | enable_uart=1 46 | dtoverlay=miniuart-bt 47 | # Set the GPU frequency so that the MiniUART (/dev/ttyS0) can be used for 48 | # Bluetooth. This has a small performance impact. See 49 | # https://www.raspberrypi.org/documentation/computers/config_txt.html#overclocking. 50 | core_freq=250 51 | -------------------------------------------------------------------------------- /custom_boot/fwup.conf: -------------------------------------------------------------------------------- 1 | # Firmware configuration file for the Raspberry Pi 3 2 | 3 | require-fwup-version="0.15.0" # For the trim() call 4 | 5 | # 6 | # Firmware metadata 7 | # 8 | 9 | # All of these can be overriden using environment variables of the same name. 10 | # 11 | # Run 'fwup -m' to query values in a .fw file. 12 | # Use 'fw_printenv' to query values on the target. 13 | # 14 | # These are used by Nerves libraries to introspect. 15 | define(NERVES_FW_PRODUCT, "Nerves Firmware") 16 | define(NERVES_FW_DESCRIPTION, "") 17 | define(NERVES_FW_VERSION, "${NERVES_SDK_VERSION}") 18 | define(NERVES_FW_PLATFORM, "rpi3") 19 | define(NERVES_FW_ARCHITECTURE, "arm") 20 | define(NERVES_FW_AUTHOR, "The Nerves Team") 21 | 22 | define(NERVES_FW_DEVPATH, "/dev/mmcblk0") 23 | define(NERVES_FW_APPLICATION_PART0_DEVPATH, "/dev/mmcblk0p3") # Linux part number is 1-based 24 | define(NERVES_FW_APPLICATION_PART0_FSTYPE, "ext4") 25 | define(NERVES_FW_APPLICATION_PART0_TARGET, "/root") 26 | define(NERVES_PROVISIONING, "${NERVES_SYSTEM}/images/fwup_include/provisioning.conf") 27 | 28 | # Default paths if not specified via the commandline 29 | define(ROOTFS, "${NERVES_SYSTEM}/images/rootfs.squashfs") 30 | 31 | # This configuration file will create an image that has an MBR and the 32 | # following 3 partitions: 33 | # 34 | # +----------------------------+ 35 | # | MBR | 36 | # +----------------------------+ 37 | # | Firmware configuration data| 38 | # | (formatted as uboot env) | 39 | # +----------------------------+ 40 | # | p0*: Boot A (FAT32) | 41 | # | zImage, bootcode.bin, | 42 | # | config.txt, etc. | 43 | # +----------------------------+ 44 | # | p0*: Boot B (FAT32) | 45 | # +----------------------------+ 46 | # | p1*: Rootfs A (squashfs) | 47 | # +----------------------------+ 48 | # | p1*: Rootfs B (squashfs) | 49 | # +----------------------------+ 50 | # | p2: Application (ext4) | 51 | # +----------------------------+ 52 | # 53 | # The p0/p1 partition points to whichever of configurations A or B that is 54 | # active. 55 | # 56 | # The image is sized to be less than 1 GB so that it fits on nearly any SDCard 57 | # around. If you have a larger SDCard and need more space, feel free to bump 58 | # the partition sizes below. 59 | 60 | # The Raspberry Pi is incredibly picky on the partition sizes and in ways that 61 | # I don't understand. Test changes one at a time to make sure that they boot. 62 | # (Sizes are in 512 byte blocks) 63 | define(UBOOT_ENV_OFFSET, 16) 64 | define(UBOOT_ENV_COUNT, 16) # 8 KB 65 | 66 | define(BOOT_A_PART_OFFSET, 63) 67 | define(BOOT_A_PART_COUNT, 38630) 68 | define-eval(BOOT_B_PART_OFFSET, "${BOOT_A_PART_OFFSET} + ${BOOT_A_PART_COUNT}") 69 | define(BOOT_B_PART_COUNT, ${BOOT_A_PART_COUNT}) 70 | 71 | # Let the rootfs have room to grow up to 128 MiB and align it to the nearest 1 72 | # MB boundary 73 | define(ROOTFS_A_PART_OFFSET, 77324) 74 | define(ROOTFS_A_PART_COUNT, 289044) 75 | define-eval(ROOTFS_B_PART_OFFSET, "${ROOTFS_A_PART_OFFSET} + ${ROOTFS_A_PART_COUNT}") 76 | define(ROOTFS_B_PART_COUNT, ${ROOTFS_A_PART_COUNT}) 77 | 78 | # Application partition. This partition can occupy all of the remaining space. 79 | # Size it to fit the destination. 80 | define-eval(APP_PART_OFFSET, "${ROOTFS_B_PART_OFFSET} + ${ROOTFS_B_PART_COUNT}") 81 | define(APP_PART_COUNT, 1048576) 82 | 83 | # Firmware archive metadata 84 | meta-product = ${NERVES_FW_PRODUCT} 85 | meta-description = ${NERVES_FW_DESCRIPTION} 86 | meta-version = ${NERVES_FW_VERSION} 87 | meta-platform = ${NERVES_FW_PLATFORM} 88 | meta-architecture = ${NERVES_FW_ARCHITECTURE} 89 | meta-author = ${NERVES_FW_AUTHOR} 90 | meta-vcs-identifier = ${NERVES_FW_VCS_IDENTIFIER} 91 | meta-misc = ${NERVES_FW_MISC} 92 | 93 | # File resources are listed in the order that they are included in the .fw file 94 | # This is important, since this is the order that they're written on a firmware 95 | # update due to the event driven nature of the update system. 96 | file-resource bootcode.bin { 97 | host-path = "${NERVES_SYSTEM}/images/rpi-firmware/bootcode.bin" 98 | } 99 | file-resource fixup.dat { 100 | host-path = "${NERVES_SYSTEM}/images/rpi-firmware/fixup_x.dat" 101 | } 102 | file-resource start.elf { 103 | host-path = "${NERVES_SYSTEM}/images/rpi-firmware/start_x.elf" 104 | } 105 | file-resource config.txt { 106 | host-path = "${NERVES_APP}/custom_boot/config.txt" 107 | } 108 | file-resource cmdline.txt { 109 | host-path = "${NERVES_SYSTEM}/images/cmdline.txt" 110 | } 111 | file-resource zImage { 112 | host-path = "${NERVES_SYSTEM}/images/zImage" 113 | } 114 | file-resource bcm2710-rpi-3-b.dtb { 115 | host-path = "${NERVES_SYSTEM}/images/bcm2710-rpi-3-b.dtb" 116 | } 117 | file-resource bcm2710-rpi-3-b-plus.dtb { 118 | host-path = "${NERVES_SYSTEM}/images/bcm2710-rpi-3-b-plus.dtb" 119 | } 120 | file-resource rpi-ft5406.dtbo { 121 | host-path = "${NERVES_SYSTEM}/images/rpi-firmware/overlays/rpi-ft5406.dtbo" 122 | } 123 | file-resource rpi-backlight.dtbo { 124 | host-path = "${NERVES_SYSTEM}/images/rpi-firmware/overlays/rpi-backlight.dtbo" 125 | } 126 | file-resource bcm2710-rpi-cm3.dtb { 127 | host-path = "${NERVES_SYSTEM}/images/bcm2710-rpi-cm3.dtb" 128 | } 129 | file-resource w1-gpio-pullup.dtbo { 130 | host-path = "${NERVES_SYSTEM}/images/rpi-firmware/overlays/w1-gpio-pullup.dtbo" 131 | } 132 | file-resource miniuart-bt.dtbo { 133 | host-path = "${NERVES_SYSTEM}/images/rpi-firmware/overlays/miniuart-bt.dtbo" 134 | } 135 | file-resource ramoops.dtbo { 136 | host-path = "${NERVES_SYSTEM}/images/ramoops.dtb" 137 | } 138 | 139 | file-resource rootfs.img { 140 | host-path = ${ROOTFS} 141 | 142 | # Error out if the rootfs size exceeds the partition size 143 | assert-size-lte = ${ROOTFS_A_PART_COUNT} 144 | } 145 | 146 | mbr mbr-a { 147 | partition 0 { 148 | block-offset = ${BOOT_A_PART_OFFSET} 149 | block-count = ${BOOT_A_PART_COUNT} 150 | type = 0xc # FAT32 151 | boot = true 152 | } 153 | partition 1 { 154 | block-offset = ${ROOTFS_A_PART_OFFSET} 155 | block-count = ${ROOTFS_A_PART_COUNT} 156 | type = 0x83 # Linux 157 | } 158 | partition 2 { 159 | block-offset = ${APP_PART_OFFSET} 160 | block-count = ${APP_PART_COUNT} 161 | type = 0x83 # Linux 162 | expand = true 163 | } 164 | # partition 3 is unused 165 | } 166 | 167 | mbr mbr-b { 168 | partition 0 { 169 | block-offset = ${BOOT_B_PART_OFFSET} 170 | block-count = ${BOOT_B_PART_COUNT} 171 | type = 0xc # FAT32 172 | boot = true 173 | } 174 | partition 1 { 175 | block-offset = ${ROOTFS_B_PART_OFFSET} 176 | block-count = ${ROOTFS_B_PART_COUNT} 177 | type = 0x83 # Linux 178 | } 179 | partition 2 { 180 | block-offset = ${APP_PART_OFFSET} 181 | block-count = ${APP_PART_COUNT} 182 | type = 0x83 # Linux 183 | expand = true 184 | } 185 | # partition 3 is unused 186 | } 187 | 188 | # Location where installed firmware information is stored. 189 | # While this is called "u-boot", u-boot isn't involved in this 190 | # setup. It just provides a convenient key/value store format. 191 | uboot-environment uboot-env { 192 | block-offset = ${UBOOT_ENV_OFFSET} 193 | block-count = ${UBOOT_ENV_COUNT} 194 | } 195 | 196 | # This firmware task writes everything to the destination media 197 | task complete { 198 | # Only match if not mounted 199 | require-unmounted-destination = true 200 | 201 | on-init { 202 | mbr_write(mbr-a) 203 | 204 | fat_mkfs(${BOOT_A_PART_OFFSET}, ${BOOT_A_PART_COUNT}) 205 | fat_setlabel(${BOOT_A_PART_OFFSET}, "BOOT-A") 206 | fat_mkdir(${BOOT_A_PART_OFFSET}, "overlays") 207 | 208 | uboot_clearenv(uboot-env) 209 | 210 | include("${NERVES_PROVISIONING}") 211 | 212 | uboot_setenv(uboot-env, "nerves_fw_active", "a") 213 | uboot_setenv(uboot-env, "nerves_fw_devpath", ${NERVES_FW_DEVPATH}) 214 | uboot_setenv(uboot-env, "a.nerves_fw_application_part0_devpath", ${NERVES_FW_APPLICATION_PART0_DEVPATH}) 215 | uboot_setenv(uboot-env, "a.nerves_fw_application_part0_fstype", ${NERVES_FW_APPLICATION_PART0_FSTYPE}) 216 | uboot_setenv(uboot-env, "a.nerves_fw_application_part0_target", ${NERVES_FW_APPLICATION_PART0_TARGET}) 217 | uboot_setenv(uboot-env, "a.nerves_fw_product", ${NERVES_FW_PRODUCT}) 218 | uboot_setenv(uboot-env, "a.nerves_fw_description", ${NERVES_FW_DESCRIPTION}) 219 | uboot_setenv(uboot-env, "a.nerves_fw_version", ${NERVES_FW_VERSION}) 220 | uboot_setenv(uboot-env, "a.nerves_fw_platform", ${NERVES_FW_PLATFORM}) 221 | uboot_setenv(uboot-env, "a.nerves_fw_architecture", ${NERVES_FW_ARCHITECTURE}) 222 | uboot_setenv(uboot-env, "a.nerves_fw_author", ${NERVES_FW_AUTHOR}) 223 | uboot_setenv(uboot-env, "a.nerves_fw_vcs_identifier", ${NERVES_FW_VCS_IDENTIFIER}) 224 | uboot_setenv(uboot-env, "a.nerves_fw_misc", ${NERVES_FW_MISC}) 225 | uboot_setenv(uboot-env, "a.nerves_fw_uuid", "\${FWUP_META_UUID}") 226 | } 227 | 228 | on-resource config.txt { fat_write(${BOOT_A_PART_OFFSET}, "config.txt") } 229 | on-resource cmdline.txt { fat_write(${BOOT_A_PART_OFFSET}, "cmdline.txt") } 230 | on-resource bootcode.bin { fat_write(${BOOT_A_PART_OFFSET}, "bootcode.bin") } 231 | on-resource start.elf { fat_write(${BOOT_A_PART_OFFSET}, "start.elf") } 232 | on-resource fixup.dat { fat_write(${BOOT_A_PART_OFFSET}, "fixup.dat") } 233 | on-resource zImage { fat_write(${BOOT_A_PART_OFFSET}, "zImage") } 234 | on-resource bcm2710-rpi-3-b.dtb { fat_write(${BOOT_A_PART_OFFSET}, "bcm2710-rpi-3-b.dtb") } 235 | on-resource bcm2710-rpi-3-b-plus.dtb { fat_write(${BOOT_A_PART_OFFSET}, "bcm2710-rpi-3-b-plus.dtb") } 236 | on-resource rpi-ft5406.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/rpi-ft5406.dtbo") } 237 | on-resource rpi-backlight.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/rpi-backlight.dtbo") } 238 | on-resource bcm2710-rpi-cm3.dtb { fat_write(${BOOT_A_PART_OFFSET}, "bcm2710-rpi-cm3.dtb") } 239 | on-resource w1-gpio-pullup.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/w1-gpio-pullup.dtbo") } 240 | on-resource miniuart-bt.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/miniuart-bt.dtbo") } 241 | on-resource ramoops.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/ramoops.dtbo") } 242 | 243 | on-resource rootfs.img { 244 | # write to the first rootfs partition 245 | raw_write(${ROOTFS_A_PART_OFFSET}) 246 | } 247 | 248 | on-finish { 249 | # Clear out any old data in the B partition that might be mistaken for 250 | # a file system. This is mostly to avoid confusion in humans when 251 | # reprogramming SDCards with unknown contents. 252 | raw_memset(${BOOT_B_PART_OFFSET}, 256, 0xff) 253 | raw_memset(${ROOTFS_B_PART_OFFSET}, 256, 0xff) 254 | 255 | # Invalidate the application data partition so that it is guaranteed to 256 | # trigger the corrupt filesystem detection code on first boot and get 257 | # formatted. If this isn't done and an old SDCard is reused, the 258 | # application data could be in a weird state. 259 | raw_memset(${APP_PART_OFFSET}, 256, 0xff) 260 | } 261 | } 262 | 263 | task upgrade.a { 264 | # This task upgrades the A partition 265 | require-partition-offset(1, ${ROOTFS_B_PART_OFFSET}) 266 | 267 | # Verify the expected platform/architecture 268 | require-uboot-variable(uboot-env, "b.nerves_fw_platform", "${NERVES_FW_PLATFORM}") 269 | require-uboot-variable(uboot-env, "b.nerves_fw_architecture", "${NERVES_FW_ARCHITECTURE}") 270 | 271 | on-init { 272 | info("Upgrading partition A") 273 | 274 | # Clear some firmware information just in case this update gets 275 | # interrupted midway. If this partition was bootable, it's not going to 276 | # be soon. 277 | uboot_unsetenv(uboot-env, "a.nerves_fw_version") 278 | uboot_unsetenv(uboot-env, "a.nerves_fw_platform") 279 | uboot_unsetenv(uboot-env, "a.nerves_fw_architecture") 280 | uboot_unsetenv(uboot-env, "a.nerves_fw_uuid") 281 | 282 | # Reset the previous contents of the A boot partition 283 | fat_mkfs(${BOOT_A_PART_OFFSET}, ${BOOT_A_PART_COUNT}) 284 | fat_setlabel(${BOOT_A_PART_OFFSET}, "BOOT-A") 285 | fat_mkdir(${BOOT_A_PART_OFFSET}, "overlays") 286 | 287 | # Indicate that the entire partition can be cleared 288 | trim(${ROOTFS_A_PART_OFFSET}, ${ROOTFS_A_PART_COUNT}) 289 | } 290 | 291 | # Write the new boot partition files and rootfs. The MBR still points 292 | # to the B partition, so an error or power failure during this part 293 | # won't hurt anything. 294 | on-resource config.txt { fat_write(${BOOT_A_PART_OFFSET}, "config.txt") } 295 | on-resource cmdline.txt { fat_write(${BOOT_A_PART_OFFSET}, "cmdline.txt") } 296 | on-resource bootcode.bin { fat_write(${BOOT_A_PART_OFFSET}, "bootcode.bin") } 297 | on-resource start.elf { fat_write(${BOOT_A_PART_OFFSET}, "start.elf") } 298 | on-resource fixup.dat { fat_write(${BOOT_A_PART_OFFSET}, "fixup.dat") } 299 | on-resource zImage { fat_write(${BOOT_A_PART_OFFSET}, "zImage") } 300 | on-resource bcm2710-rpi-3-b.dtb { fat_write(${BOOT_A_PART_OFFSET}, "bcm2710-rpi-3-b.dtb") } 301 | on-resource bcm2710-rpi-3-b-plus.dtb { fat_write(${BOOT_A_PART_OFFSET}, "bcm2710-rpi-3-b-plus.dtb") } 302 | on-resource rpi-ft5406.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/rpi-ft5406.dtbo") } 303 | on-resource rpi-backlight.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/rpi-backlight.dtbo") } 304 | on-resource bcm2710-rpi-cm3.dtb { fat_write(${BOOT_A_PART_OFFSET}, "bcm2710-rpi-cm3.dtb") } 305 | on-resource w1-gpio-pullup.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/w1-gpio-pullup.dtbo") } 306 | on-resource miniuart-bt.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/miniuart-bt.dtbo") } 307 | on-resource ramoops.dtbo { fat_write(${BOOT_A_PART_OFFSET}, "overlays/ramoops.dtbo") } 308 | on-resource rootfs.img { 309 | delta-source-raw-offset=${ROOTFS_B_PART_OFFSET} 310 | delta-source-raw-count=${ROOTFS_B_PART_COUNT} 311 | raw_write(${ROOTFS_A_PART_OFFSET}) 312 | } 313 | 314 | on-finish { 315 | # Update firmware metadata 316 | uboot_setenv(uboot-env, "a.nerves_fw_application_part0_devpath", ${NERVES_FW_APPLICATION_PART0_DEVPATH}) 317 | uboot_setenv(uboot-env, "a.nerves_fw_application_part0_fstype", ${NERVES_FW_APPLICATION_PART0_FSTYPE}) 318 | uboot_setenv(uboot-env, "a.nerves_fw_application_part0_target", ${NERVES_FW_APPLICATION_PART0_TARGET}) 319 | uboot_setenv(uboot-env, "a.nerves_fw_product", ${NERVES_FW_PRODUCT}) 320 | uboot_setenv(uboot-env, "a.nerves_fw_description", ${NERVES_FW_DESCRIPTION}) 321 | uboot_setenv(uboot-env, "a.nerves_fw_version", ${NERVES_FW_VERSION}) 322 | uboot_setenv(uboot-env, "a.nerves_fw_platform", ${NERVES_FW_PLATFORM}) 323 | uboot_setenv(uboot-env, "a.nerves_fw_architecture", ${NERVES_FW_ARCHITECTURE}) 324 | uboot_setenv(uboot-env, "a.nerves_fw_author", ${NERVES_FW_AUTHOR}) 325 | uboot_setenv(uboot-env, "a.nerves_fw_vcs_identifier", ${NERVES_FW_VCS_IDENTIFIER}) 326 | uboot_setenv(uboot-env, "a.nerves_fw_misc", ${NERVES_FW_MISC}) 327 | uboot_setenv(uboot-env, "a.nerves_fw_uuid", "\${FWUP_META_UUID}") 328 | 329 | # Switch over to boot the new firmware 330 | uboot_setenv(uboot-env, "nerves_fw_active", "a") 331 | mbr_write(mbr-a) 332 | } 333 | 334 | on-error { 335 | } 336 | } 337 | 338 | task upgrade.b { 339 | # This task upgrades the B partition 340 | require-partition-offset(1, ${ROOTFS_A_PART_OFFSET}) 341 | 342 | # Verify the expected platform/architecture 343 | require-uboot-variable(uboot-env, "a.nerves_fw_platform", "${NERVES_FW_PLATFORM}") 344 | require-uboot-variable(uboot-env, "a.nerves_fw_architecture", "${NERVES_FW_ARCHITECTURE}") 345 | 346 | on-init { 347 | info("Upgrading partition B") 348 | 349 | # Clear some firmware information just in case this update gets 350 | # interrupted midway. 351 | uboot_unsetenv(uboot-env, "b.nerves_fw_version") 352 | uboot_unsetenv(uboot-env, "b.nerves_fw_platform") 353 | uboot_unsetenv(uboot-env, "b.nerves_fw_architecture") 354 | uboot_unsetenv(uboot-env, "b.nerves_fw_uuid") 355 | 356 | # Reset the previous contents of the B boot partition 357 | fat_mkfs(${BOOT_B_PART_OFFSET}, ${BOOT_B_PART_COUNT}) 358 | fat_setlabel(${BOOT_B_PART_OFFSET}, "BOOT-B") 359 | fat_mkdir(${BOOT_B_PART_OFFSET}, "overlays") 360 | 361 | trim(${ROOTFS_B_PART_OFFSET}, ${ROOTFS_B_PART_COUNT}) 362 | } 363 | 364 | # Write the new boot partition files and rootfs. The MBR still points 365 | # to the A partition, so an error or power failure during this part 366 | # won't hurt anything. 367 | on-resource config.txt { fat_write(${BOOT_B_PART_OFFSET}, "config.txt") } 368 | on-resource cmdline.txt { fat_write(${BOOT_B_PART_OFFSET}, "cmdline.txt") } 369 | on-resource bootcode.bin { fat_write(${BOOT_B_PART_OFFSET}, "bootcode.bin") } 370 | on-resource start.elf { fat_write(${BOOT_B_PART_OFFSET}, "start.elf") } 371 | on-resource fixup.dat { fat_write(${BOOT_B_PART_OFFSET}, "fixup.dat") } 372 | on-resource zImage { fat_write(${BOOT_B_PART_OFFSET}, "zImage") } 373 | on-resource bcm2710-rpi-3-b.dtb { fat_write(${BOOT_B_PART_OFFSET}, "bcm2710-rpi-3-b.dtb") } 374 | on-resource bcm2710-rpi-3-b-plus.dtb { fat_write(${BOOT_B_PART_OFFSET}, "bcm2710-rpi-3-b-plus.dtb") } 375 | on-resource rpi-ft5406.dtbo { fat_write(${BOOT_B_PART_OFFSET}, "overlays/rpi-ft5406.dtbo") } 376 | on-resource rpi-backlight.dtbo { fat_write(${BOOT_B_PART_OFFSET}, "overlays/rpi-backlight.dtbo") } 377 | on-resource bcm2710-rpi-cm3.dtb { fat_write(${BOOT_B_PART_OFFSET}, "bcm2710-rpi-cm3.dtb") } 378 | on-resource w1-gpio-pullup.dtbo { fat_write(${BOOT_B_PART_OFFSET}, "overlays/w1-gpio-pullup.dtbo") } 379 | on-resource miniuart-bt.dtbo { fat_write(${BOOT_B_PART_OFFSET}, "overlays/miniuart-bt.dtbo") } 380 | on-resource ramoops.dtbo { fat_write(${BOOT_B_PART_OFFSET}, "overlays/ramoops.dtbo") } 381 | on-resource rootfs.img { 382 | delta-source-raw-offset=${ROOTFS_A_PART_OFFSET} 383 | delta-source-raw-count=${ROOTFS_A_PART_COUNT} 384 | raw_write(${ROOTFS_B_PART_OFFSET}) 385 | } 386 | 387 | on-finish { 388 | # Update firmware metadata 389 | uboot_setenv(uboot-env, "b.nerves_fw_application_part0_devpath", ${NERVES_FW_APPLICATION_PART0_DEVPATH}) 390 | uboot_setenv(uboot-env, "b.nerves_fw_application_part0_fstype", ${NERVES_FW_APPLICATION_PART0_FSTYPE}) 391 | uboot_setenv(uboot-env, "b.nerves_fw_application_part0_target", ${NERVES_FW_APPLICATION_PART0_TARGET}) 392 | uboot_setenv(uboot-env, "b.nerves_fw_product", ${NERVES_FW_PRODUCT}) 393 | uboot_setenv(uboot-env, "b.nerves_fw_description", ${NERVES_FW_DESCRIPTION}) 394 | uboot_setenv(uboot-env, "b.nerves_fw_version", ${NERVES_FW_VERSION}) 395 | uboot_setenv(uboot-env, "b.nerves_fw_platform", ${NERVES_FW_PLATFORM}) 396 | uboot_setenv(uboot-env, "b.nerves_fw_architecture", ${NERVES_FW_ARCHITECTURE}) 397 | uboot_setenv(uboot-env, "b.nerves_fw_author", ${NERVES_FW_AUTHOR}) 398 | uboot_setenv(uboot-env, "b.nerves_fw_vcs_identifier", ${NERVES_FW_VCS_IDENTIFIER}) 399 | uboot_setenv(uboot-env, "b.nerves_fw_misc", ${NERVES_FW_MISC}) 400 | uboot_setenv(uboot-env, "b.nerves_fw_uuid", "\${FWUP_META_UUID}") 401 | 402 | # Switch over to boot the new firmware 403 | uboot_setenv(uboot-env, "nerves_fw_active", "b") 404 | mbr_write(mbr-b) 405 | } 406 | 407 | on-error { 408 | } 409 | } 410 | 411 | task upgrade.unexpected { 412 | require-uboot-variable(uboot-env, "a.nerves_fw_platform", "${NERVES_FW_PLATFORM}") 413 | require-uboot-variable(uboot-env, "a.nerves_fw_architecture", "${NERVES_FW_ARCHITECTURE}") 414 | on-init { 415 | error("Please check the media being upgraded. It doesn't look like either the A or B partitions are active.") 416 | } 417 | } 418 | 419 | task upgrade.wrongplatform { 420 | on-init { 421 | error("Expecting platform=${NERVES_FW_PLATFORM} and architecture=${NERVES_FW_ARCHITECTURE}") 422 | } 423 | } 424 | 425 | task provision { 426 | require-uboot-variable(uboot-env, "a.nerves_fw_platform", "${NERVES_FW_PLATFORM}") 427 | require-uboot-variable(uboot-env, "a.nerves_fw_architecture", "${NERVES_FW_ARCHITECTURE}") 428 | on-init { 429 | include("${NERVES_PROVISIONING}") 430 | } 431 | } 432 | task provision.wrongplatform { 433 | on-init { 434 | error("Expecting platform=${NERVES_FW_PLATFORM} and architecture=${NERVES_FW_ARCHITECTURE}") 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /lib/xdoor.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor do 2 | def logins() do 3 | Application.get_env(:xdoor, :storage_dir) 4 | |> Path.join("logins") 5 | |> File.read!() 6 | |> IO.puts() 7 | end 8 | 9 | def lock_state_changes() do 10 | Application.get_env(:xdoor, :storage_dir) 11 | |> Path.join("lock_state_changes") 12 | |> File.read!() 13 | |> IO.puts() 14 | end 15 | 16 | def logs() do 17 | RingLogger.get() 18 | |> Enum.map(&RingLogger.format/1) 19 | |> IO.puts() 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/xdoor/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.Application do 2 | use Application 3 | 4 | @mqtt_config [ 5 | mqtt_host: "homeassistant.lan.xhain.space", 6 | username: "homeassistant", 7 | password: File.read!("secrets/mqtt_pw") |> String.trim(), 8 | client_id: "xdoor" 9 | ] 10 | 11 | def start(_type, _args) do 12 | ensure_storage_dir() 13 | 14 | children = [ 15 | Xdoor.Monitor, 16 | Xdoor.SSHServer, 17 | Xdoor.AuthorizedKeys, 18 | {ExHomeassistant, @mqtt_config}, 19 | Xdoor.LockState 20 | ] 21 | 22 | opts = [strategy: :one_for_one, name: Xdoor.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | def ensure_storage_dir() do 27 | storage_dir = Application.fetch_env!(:xdoor, :storage_dir) 28 | 29 | if !File.exists?(storage_dir) do 30 | File.mkdir_p!(storage_dir) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/xdoor/authorized_keys.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.AuthorizedKeys do 2 | use GenServer 3 | require Logger 4 | 5 | @update_interval_ms Application.compile_env!(:xdoor, :authorized_keys_update_interval_ms) 6 | @perist_to_filename Application.compile_env!(:xdoor, :storage_dir) |> Path.join("authorized_keys") 7 | @log_dir Application.compile_env!(:xdoor, :storage_dir) |> Path.join("logs") 8 | 9 | def list() do 10 | Application.get_env(:xdoor, :authorized_keys, []) 11 | end 12 | 13 | def list_admin() do 14 | Application.get_env(:xdoor, :authorized_keys_admin, []) 15 | end 16 | 17 | def start_link(_) do 18 | GenServer.start_link(__MODULE__, []) 19 | end 20 | 21 | def init(_) do 22 | if File.exists?(@perist_to_filename) do 23 | authorized_keys = File.read!(@perist_to_filename) 24 | Application.put_env(:xdoor, :authorized_keys, :ssh_file.decode(authorized_keys, :auth_keys)) 25 | end 26 | 27 | admin_keys = 28 | Application.get_env(:nerves_ssh, :authorized_keys, []) 29 | |> Enum.flat_map(&:ssh_file.decode(&1, :auth_keys)) 30 | 31 | Application.put_env(:xdoor, :authorized_keys_admin, admin_keys) 32 | 33 | schedule_update() 34 | {:ok, %{}} 35 | end 36 | 37 | def handle_info(:update, state) do 38 | schedule_update() 39 | {:noreply, state} 40 | end 41 | 42 | defp schedule_update() do 43 | Process.send_after(self(), :update, @update_interval_ms) 44 | spawn(fn -> update() end) 45 | end 46 | 47 | def update() do 48 | Logger.info("Updating authorized keys") 49 | %Req.Response{status: 200, body: authorized_keys} = Req.get!("https://xdoor.x-hain.de/authorized_keys") 50 | %Req.Response{status: 200, body: signature} = Req.get!("https://xdoor.x-hain.de/authorized_keys.sig") 51 | 52 | public_key = 53 | :code.priv_dir(:xdoor) 54 | |> Path.join("authorized_keys_pub.pem") 55 | |> ExPublicKey.load!() 56 | 57 | ExPublicKey.verify(authorized_keys, Base.decode64!(signature), public_key) 58 | |> case do 59 | {:ok, true} -> 60 | Logger.debug("Fetching authorized_keys: Valid signature") 61 | 62 | current_keys = Application.get_env(:xdoor, :authorized_keys, "") 63 | new_keys = :ssh_file.decode(authorized_keys, :auth_keys) 64 | 65 | Application.put_env(:xdoor, :authorized_keys_last_update, System.os_time(:millisecond)) 66 | 67 | if :erlang.phash2(new_keys) != :erlang.phash2(current_keys) do 68 | Application.put_env(:xdoor, :authorized_keys, new_keys) 69 | File.write!(@perist_to_filename, authorized_keys) 70 | Logger.info("Authorized keys changed") 71 | else 72 | Logger.info("No changes to authorized keys") 73 | end 74 | 75 | error -> 76 | Logger.error("Error validating signature of authorized_keys: #{inspect(error)}") 77 | end 78 | end 79 | 80 | def persist_logs() do 81 | File.mkdir(@log_dir) 82 | date_str = DateTime.utc_now() |> DateTime.to_iso8601() 83 | log_filename = Path.join(@log_dir, "#{date_str}.logs") 84 | RingLogger.save(log_filename) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/xdoor/lock_control.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.LockControl do 2 | require Logger 3 | 4 | @greeting :code.priv_dir(:xdoor) |> Path.join("greeting") |> File.read!() 5 | 6 | def open() do 7 | IO.write(@greeting) 8 | IO.write("OPENING DOOR\n") 9 | toggle_gpio(23) 10 | Logger.info("Door is opening") 11 | end 12 | 13 | def close() do 14 | IO.write(@greeting) 15 | IO.write("CLOSING DOOR\n") 16 | toggle_gpio(24) 17 | Logger.info("Door is closing") 18 | end 19 | 20 | defp toggle_gpio(pin_number) do 21 | if Application.get_env(:xdoor, :gpio_enabled, false) do 22 | {:ok, gpio} = Circuits.GPIO.open(pin_number, :output) 23 | Circuits.GPIO.write(gpio, 1) 24 | :timer.sleep(1000) 25 | Circuits.GPIO.write(gpio, 0) 26 | Circuits.GPIO.close(gpio) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/xdoor/lock_state.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.LockState do 2 | use GenServer 3 | require Logger 4 | 5 | alias ExHomeassistant.Devices.Switch 6 | 7 | @poll_frequency_ms 200 8 | @gpio_lock_sensor 8 9 | 10 | @xdoor_switch %Switch{ 11 | name: "xDoor Lock" 12 | } 13 | 14 | def locked?() do 15 | Application.get_env(:xdoor, :lock_state) 16 | end 17 | 18 | def start_link(_) do 19 | GenServer.start_link(__MODULE__, []) 20 | end 21 | 22 | def init(_) do 23 | {:ok, gpio} = Circuits.GPIO.open(@gpio_lock_sensor, :input) 24 | Circuits.GPIO.set_pull_mode(gpio, :pullup) 25 | state = %{gpio: gpio} 26 | poll_gpio(state) 27 | Switch.configure(@xdoor_switch) 28 | Switch.subscribe(@xdoor_switch) 29 | Switch.set_state(@xdoor_switch, locked?()) 30 | {:ok, state} 31 | end 32 | 33 | def handle_info(:poll_gpio, state) do 34 | poll_gpio(state) 35 | {:noreply, state} 36 | end 37 | 38 | def handle_info({:homeassistant_command, _, _} = event, state) do 39 | Logger.info("Received HomeAssistant command: #{inspect(event)}") 40 | 41 | case Switch.parse_event(@xdoor_switch, event) do 42 | true -> Xdoor.LockControl.close() 43 | _ -> :noop 44 | end 45 | 46 | {:noreply, state} 47 | end 48 | 49 | defp poll_gpio(%{gpio: gpio}) do 50 | Process.send_after(self(), :poll_gpio, @poll_frequency_ms) 51 | 52 | last_state = locked?() 53 | 54 | current_state = 55 | case Circuits.GPIO.read(gpio) do 56 | 0 -> true 57 | 1 -> false 58 | end 59 | 60 | if last_state != current_state do 61 | Logger.info("Locked? state changed from :#{last_state} to #{current_state}") 62 | on_state_change(current_state) 63 | 64 | Application.put_env(:xdoor, :lock_state, current_state) 65 | end 66 | end 67 | 68 | defp on_state_change(state) do 69 | log(state) 70 | Switch.set_state(@xdoor_switch, state) 71 | end 72 | 73 | defp log(state) do 74 | file = 75 | Application.fetch_env!(:xdoor, :storage_dir) 76 | |> Path.join("lock_state_changes") 77 | |> File.open!([:append]) 78 | 79 | IO.puts(file, "#{DateTime.utc_now() |> DateTime.to_iso8601()};#{state}") 80 | File.close(file) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/xdoor/monitor.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.Monitor do 2 | use GenServer 3 | require Logger 4 | 5 | def start_link(_) do 6 | GenServer.start_link(__MODULE__, []) 7 | end 8 | 9 | @network_message_name ["interface", "eth0", "connection"] 10 | @tick_interval 60000 11 | 12 | def init(_) do 13 | if Application.get_env(:xdoor, :enable_monitor, false) do 14 | Logger.info("Starting #{__MODULE__}") 15 | VintageNet.subscribe(@network_message_name) 16 | 17 | initial_state = %{ 18 | error_since: nil 19 | } 20 | 21 | state = 22 | VintageNet.get(@network_message_name) 23 | |> handle_network_state(initial_state) 24 | 25 | :timer.send_interval(@tick_interval, :tick) 26 | 27 | {:ok, state} 28 | else 29 | :ignore 30 | end 31 | end 32 | 33 | def handle_info({VintageNet, @network_message_name, old_value, new_value, _metadata}, state) do 34 | Logger.info("#{__MODULE__}: new connection state: #{inspect(new_value)} (old: #{inspect(old_value)})") 35 | 36 | {:noreply, handle_network_state(new_value, state)} 37 | end 38 | 39 | def handle_info(:tick, state) do 40 | System.cmd("vcgencmd", ["get_throttled"]) 41 | |> handle_vcgencmd_response() 42 | 43 | {:noreply, state} 44 | end 45 | 46 | defp handle_network_state(:internet, state) do 47 | Nerves.Leds.set(:red, true) 48 | %{state | error_since: nil} 49 | end 50 | 51 | defp handle_network_state(:lan, state) do 52 | Nerves.Leds.set(:red, :slowblink) 53 | 54 | state 55 | |> maybe_set_error() 56 | |> maybe_restart 57 | end 58 | 59 | defp handle_network_state(_network_state, state) do 60 | Nerves.Leds.set(:red, :fastblink) 61 | 62 | state 63 | |> maybe_set_error() 64 | |> maybe_restart() 65 | end 66 | 67 | defp handle_vcgencmd_response({"throttled=0x" <> hex, 0}) do 68 | # see https://www.raspberrypi.org/documentation/raspbian/applications/vcgencmd.md 69 | <<_::4, past::4, _::12, current::4>> = hex |> String.trim() |> String.pad_leading(6, "0") |> Base.decode16!() 70 | 71 | case {past, current} do 72 | {0, 0} -> 73 | # all good 74 | Nerves.Leds.set(:green, true) 75 | 76 | {_, 0} -> 77 | # past bad, current good 78 | Logger.info("#{__MODULE__}: System unhealthy, vcgencmd current: #{inspect(current)}, past: #{inspect(past)}") 79 | Nerves.Leds.set(:green, :slowblink) 80 | 81 | {_, _} -> 82 | # past and current bad 83 | Logger.error("#{__MODULE__}: System unhealthy, vcgencmd current: #{inspect(current)}, past: #{inspect(past)}") 84 | 85 | Nerves.Leds.set(:green, :fastblink) 86 | end 87 | end 88 | 89 | defp handle_vcgencmd_response(error) do 90 | Logger.error("#{__MODULE__}: Unexpected vcgencmd resonse: #{inspect(error)}") 91 | Nerves.Leds.set(:green, :fastblink) 92 | end 93 | 94 | defp maybe_set_error(%{error_since: nil} = state), 95 | do: %{state | error_since: System.os_time(:millisecond)} 96 | 97 | defp maybe_set_error(state), do: state 98 | 99 | @restart_timeout_ms 5 * 60 * 1000 100 | defp maybe_restart(%{error_since: nil} = state), do: state 101 | 102 | defp maybe_restart(%{error_since: millis} = state) do 103 | if System.os_time(:millisecond) - millis > @restart_timeout_ms do 104 | Nerves.Runtime.reboot() 105 | end 106 | 107 | state 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/xdoor/motion_detection.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.MotionDetection do 2 | use GenServer 3 | require Logger 4 | alias Xdoor.{LockState, LockControl} 5 | 6 | @poll_frequency_ms 200 7 | @gpio_motion_sensor 7 8 | @no_motion_threshold_ms 5 * 60 * 1000 9 | 10 | def last_motion() do 11 | Application.get_env(:xdoor, :last_motion, 0) 12 | end 13 | 14 | def reset_last_motion() do 15 | Logger.info("Resetting last motion") 16 | Application.put_env(:xdoor, :last_motion, System.os_time(:millisecond)) 17 | end 18 | 19 | def start_link(_) do 20 | GenServer.start_link(__MODULE__, []) 21 | end 22 | 23 | def init(_) do 24 | {:ok, gpio} = Circuits.GPIO.open(@gpio_motion_sensor, :input) 25 | state = %{gpio: gpio} 26 | poll_gpio(state) 27 | {:ok, state} 28 | end 29 | 30 | def handle_info(:poll_gpio, state) do 31 | poll_gpio(state) 32 | {:noreply, state} 33 | end 34 | 35 | defp poll_gpio(%{gpio: gpio}) do 36 | Process.send_after(self(), :poll_gpio, @poll_frequency_ms) 37 | 38 | last_motion = last_motion() 39 | current_time = System.os_time(:millisecond) 40 | 41 | case Circuits.GPIO.read(gpio) do 42 | 0 -> 43 | if current_time > last_motion + @no_motion_threshold_ms do 44 | Logger.debug("No Motion Detected, above threshold. #{current_time} #{last_motion} #{@no_motion_threshold_ms}") 45 | 46 | if LockState.locked?() do 47 | # Logger.debug("Lock already closed") 48 | reset_last_motion() 49 | :timer.sleep(5000) 50 | else 51 | Logger.info("No motion detected for #{inspect(@no_motion_threshold_ms)} ms and lock is open, closing") 52 | LockControl.close() 53 | :timer.sleep(5000) 54 | end 55 | else 56 | Logger.debug("No Motion Detected, below threshold") 57 | end 58 | 59 | 1 -> 60 | Application.put_env(:xdoor, :last_motion, current_time) 61 | Logger.debug("Motion Detected") 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/xdoor/ssh_keys.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.SSHKeys do 2 | require Logger 3 | 4 | def host_key(algorithm, options) do 5 | :ssh_file.host_key(algorithm, options) 6 | end 7 | 8 | def is_auth_key(key, user, _options) when user in ['open', 'close'] do 9 | Xdoor.AuthorizedKeys.list() 10 | |> Enum.find(fn {k, _info} -> k == key end) 11 | |> case do 12 | {_key, info} -> 13 | log(user, info) 14 | true 15 | 16 | _ -> 17 | false 18 | end 19 | end 20 | 21 | def is_auth_key(key, 'admin' = user, _options) do 22 | Logger.debug("Admin login attempt") 23 | 24 | Xdoor.AuthorizedKeys.list_admin() 25 | |> Enum.find(fn {k, _info} -> k == key end) 26 | |> case do 27 | {_key, info} -> 28 | log(user, info) 29 | true 30 | 31 | _ -> 32 | false 33 | end 34 | end 35 | 36 | defp log(user, info) do 37 | file = 38 | Application.fetch_env!(:xdoor, :storage_dir) 39 | |> Path.join("logins") 40 | |> File.open!([:append]) 41 | 42 | IO.puts(file, "#{DateTime.utc_now() |> DateTime.to_iso8601()};#{Keyword.get(info, :comment)};#{user}") 43 | File.close(file) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/xdoor/ssh_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.SSHServer do 2 | use GenServer 3 | require Logger 4 | alias Xdoor.{SSHKeys, LockControl} 5 | 6 | def start_link(_) do 7 | GenServer.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | system_dir = :code.priv_dir(:xdoor) |> Path.join("host_key") |> to_charlist() 12 | port = Application.fetch_env!(:xdoor, :ssh_port) 13 | 14 | Logger.info("Starting xdoor ssh server on port #{port}") 15 | 16 | {:ok, server_pid} = 17 | :ssh.daemon(port, [ 18 | {:id_string, :random}, 19 | {:system_dir, system_dir}, 20 | {:user_dir, system_dir}, 21 | {:key_cb, {SSHKeys, []}}, 22 | {:shell, &start_shell/2}, 23 | {:exec, &start_exec/3}, 24 | {:parallel_login, true}, 25 | {:auth_methods, 'publickey'} 26 | ]) 27 | 28 | Process.link(server_pid) 29 | 30 | {:ok, %{server_pid: server_pid}} 31 | end 32 | 33 | def start_shell('open' = user, _peer) do 34 | Logger.info("Starting shell for user #{user}") 35 | spawn(fn -> LockControl.open() end) 36 | end 37 | 38 | def start_shell('close' = user, _peer) do 39 | Logger.info("Starting shell for user #{user}") 40 | spawn(fn -> LockControl.close() end) 41 | end 42 | 43 | def start_exec(_, 'open' = user, _peer) do 44 | Logger.info("Starting exec for user #{user}") 45 | spawn(fn -> LockControl.open() end) 46 | end 47 | 48 | def start_exec(_, 'close' = user, _peer) do 49 | Logger.info("Starting exec for user #{user}") 50 | spawn(fn -> LockControl.close() end) 51 | end 52 | 53 | def start_exec('logins', 'admin', _peer) do 54 | spawn(fn -> Xdoor.logins() end) 55 | end 56 | 57 | def start_exec('lock_state_changes', 'admin', _peer) do 58 | spawn(fn -> Xdoor.lock_state_changes() end) 59 | end 60 | 61 | def start_exec('logs', 'admin', _peer) do 62 | spawn(fn -> Xdoor.logs() end) 63 | end 64 | 65 | def start_exec(_cmd, _user, _peer) do 66 | spawn(fn -> 67 | IO.puts("Command execution not alllowed.") 68 | end) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Xdoor.MixProject do 2 | use Mix.Project 3 | 4 | @app :xdoor 5 | @version "0.1.0" 6 | @all_targets [:rpi, :rpi0, :rpi2, :rpi3, :rpi3a, :rpi4, :bbb, :x86_64] 7 | 8 | def project do 9 | [ 10 | app: @app, 11 | version: @version, 12 | elixir: "~> 1.9", 13 | archives: [nerves_bootstrap: "~> 1.9"], 14 | start_permanent: Mix.env() == :prod, 15 | build_embedded: true, 16 | aliases: [loadconfig: [&bootstrap/1]], 17 | deps: deps(), 18 | releases: [{@app, release()}], 19 | preferred_cli_target: [run: :host, test: :host] 20 | ] 21 | end 22 | 23 | # Starting nerves_bootstrap adds the required aliases to Mix.Project.config() 24 | # Aliases are only added if MIX_TARGET is set. 25 | def bootstrap(args) do 26 | Application.start(:nerves_bootstrap) 27 | Mix.Task.run("loadconfig", args) 28 | end 29 | 30 | # Run "mix help compile.app" to learn about applications. 31 | def application do 32 | [ 33 | mod: {Xdoor.Application, []}, 34 | extra_applications: [:logger, :runtime_tools, :ssh, :crypto] 35 | ] 36 | end 37 | 38 | # Run "mix help deps" to learn about dependencies. 39 | defp deps do 40 | [ 41 | # Nerves 42 | {:nerves, "~> 1.7", runtime: false}, 43 | {:shoehorn, "~> 0.7"}, 44 | {:ring_logger, "~> 0.8"}, 45 | {:toolshed, "~> 0.2"}, 46 | 47 | # Added 48 | {:circuits_gpio, "~> 1.0"}, 49 | {:ex_crypto, "~> 0.10"}, 50 | {:jason, "~> 1.0"}, 51 | {:nerves_leds, "~> 0.8"}, 52 | {:nerves_motd, "~> 0.1.0", targets: @all_targets}, 53 | {:req, "~> 0.4"}, 54 | {:ex_homeassistant, git: "git@github.com:Reimerei/ex_homeassistant.git", branch: "main"}, 55 | 56 | # Dependencies for all targets except :host 57 | {:nerves_runtime, "~> 0.11", targets: @all_targets}, 58 | {:nerves_pack, "~> 0.3", targets: @all_targets}, 59 | 60 | # Dependencies for specific targets 61 | {:nerves_system_rpi, "~> 1.12", runtime: false, targets: :rpi}, 62 | {:nerves_system_rpi0, "~> 1.12", runtime: false, targets: :rpi0}, 63 | {:nerves_system_rpi2, "~> 1.12", runtime: false, targets: :rpi2}, 64 | {:nerves_system_rpi3, "~> 1.12", runtime: false, targets: :rpi3}, 65 | {:nerves_system_rpi3a, "~> 1.12", runtime: false, targets: :rpi3a}, 66 | {:nerves_system_rpi4, "~> 1.12", runtime: false, targets: :rpi4}, 67 | {:nerves_system_bbb, "~> 2.3", runtime: false, targets: :bbb}, 68 | {:nerves_system_x86_64, "~> 1.12", runtime: false, targets: :x86_64} 69 | ] 70 | end 71 | 72 | def release do 73 | [ 74 | overwrite: true, 75 | cookie: "#{@app}_cookie", 76 | include_erts: &Nerves.Release.erts/0, 77 | steps: [&Nerves.Release.init/1, :assemble], 78 | strip_beams: Mix.env() == :prod 79 | ] 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "beam_notify": {:hex, :beam_notify, "1.1.0", "4ce38e27460a3c03b6f77c10c6f31458b035ebb1035cd52d4b3e771311837dba", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "8547a310702bfcea0e401534398617b940808ff6ad10c43dddc85c169de7b9cc"}, 3 | "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"}, 4 | "circuits_gpio": {:hex, :circuits_gpio, "1.2.2", "92a84668578f42ff557031c50fe893d14b6548543dda6d11dba8c1a808999a4f", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7516b64e68da07d76ee6bbc6d6159ce0cd31b21c13cb25dc998a4e044f6e0960"}, 5 | "circular_buffer": {:hex, :circular_buffer, "0.4.1", "477f370fd8cfe1787b0a1bade6208bbd274b34f1610e41f1180ba756a7679839", [:mix], [], "hexpm", "633ef2e059dde0d7b89bbab13b1da9d04c6685e80e68fbdf41282d4fae746b72"}, 6 | "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, 7 | "elixir_make": {:hex, :elixir_make, "0.8.3", "d38d7ee1578d722d89b4d452a3e36bcfdc644c618f0d063b874661876e708683", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "5c99a18571a756d4af7a4d89ca75c28ac899e6103af6f223982f09ce44942cc9"}, 8 | "emqtt": {:git, "https://github.com/emqx/emqtt.git", "c815a18f9be46a7cace5620ef42aec468fc47552", [tag: "1.11.0"]}, 9 | "ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"}, 10 | "ex_homeassistant": {:git, "git@github.com:Reimerei/ex_homeassistant.git", "e2b1ba358be91914b7fe1d071ceecb021f6f5002", [branch: "main"]}, 11 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 12 | "gen_state_machine": {:hex, :gen_state_machine, "3.0.0", "1e57f86a494e5c6b14137ebef26a7eb342b3b0070c7135f2d6768ed3f6b6cdff", [:mix], [], "hexpm", "0a59652574bebceb7309f6b749d2a41b45fdeda8dbb4da0791e355dd19f0ed15"}, 13 | "getopt": {:hex, :getopt, "1.0.2", "33d9b44289fe7ad08627ddfe1d798e30b2da0033b51da1b3a2d64e72cd581d02", [:rebar3], [], "hexpm", "a0029aea4322fb82a61f6876a6d9c66dc9878b6cb61faa13df3187384fd4ea26"}, 14 | "gun": {:git, "https://github.com/emqx/gun", "4faea40b9a8ca1eac5288355f8202e0cea379d50", [tag: "1.3.7"]}, 15 | "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, 16 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 17 | "mdns_lite": {:hex, :mdns_lite, "0.8.10", "d89183b737f95c243da94297c8d25460493b3ab2452100b2037987a07642070d", [:mix], [{:vintage_net, "~> 0.7", [hex: :vintage_net, repo: "hexpm", optional: true]}], "hexpm", "2723259aa4587b269a625ff61d1cff0cd30a8940288b7b998aec0e4139ec98eb"}, 18 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 19 | "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, 20 | "muontrap": {:hex, :muontrap, "1.5.0", "bf5c273872379968615a39974458328209ac97fa1f588396192131ff973d1ca2", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "daf605e877f60b5be9215e3420d7971fc468677b29921e40915b15fd928273d4"}, 21 | "nerves": {:hex, :nerves, "1.10.5", "9c4296a5fd9c48858a92b17ae52cfcf1bc9acfefdc2da5c10ec79e4c139ecaec", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a1309b6c56e2797f7514e89afc61b7c06803f359e94b00ac006bef4e611d95e9"}, 22 | "nerves_leds": {:hex, :nerves_leds, "0.8.1", "8010e5c8d59efa2123458a5daf905734a25bd7bbe54ceeeed388076e1751c469", [:mix], [], "hexpm", "c4dfe79e0578d1909933ac6486a21fd5093e5386bc93e00e971b00fbe03e7e75"}, 23 | "nerves_logging": {:hex, :nerves_logging, "0.2.2", "d0e878ac92e6907757fa9898b661250fa1cf50474763ca59ecfadca1c2235337", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "74c181c6f011ea0c2d52956ad82065a59d7c7b62ddfba5967b010ef125f460a5"}, 24 | "nerves_motd": {:hex, :nerves_motd, "0.1.13", "5ab28a458e8ba8cf7f165573dd413f3ed0f9720dc08eda51c2bcb0d7edffa9ee", [:mix], [{:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:nerves_time, "~> 0.4", [hex: :nerves_time, repo: "hexpm", optional: true]}, {:nerves_time_zones, "~> 0.1", [hex: :nerves_time_zones, repo: "hexpm", optional: true]}], "hexpm", "f3fee43ab52181b0de015eec4a9835b99526170ac83a0f621a155f458e1c9eeb"}, 25 | "nerves_pack": {:hex, :nerves_pack, "0.7.0", "bc93834edbb9321b180dc104440070279eb02159359715f68f770e74ed86a582", [:mix], [{:mdns_lite, "~> 0.8", [hex: :mdns_lite, repo: "hexpm", optional: false]}, {:nerves_motd, "~> 0.1", [hex: :nerves_motd, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.6", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:nerves_ssh, "~> 0.3", [hex: :nerves_ssh, repo: "hexpm", optional: false]}, {:nerves_time, "~> 0.3", [hex: :nerves_time, repo: "hexpm", optional: false]}, {:ring_logger, "~> 0.8", [hex: :ring_logger, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.10", [hex: :vintage_net, repo: "hexpm", optional: false]}, {:vintage_net_direct, "~> 0.10", [hex: :vintage_net_direct, repo: "hexpm", optional: false]}, {:vintage_net_ethernet, "~> 0.10", [hex: :vintage_net_ethernet, repo: "hexpm", optional: false]}, {:vintage_net_wifi, "~> 0.10", [hex: :vintage_net_wifi, repo: "hexpm", optional: false]}], "hexpm", "65a43ea78c10938c87c72d6d42a82c05e831e9a95a0ea26fe8f9d848c009cc57"}, 26 | "nerves_runtime": {:hex, :nerves_runtime, "0.13.7", "0a7b15d5f55af1b695f7a4a1bd597c2f6101f8cbe7fe7a30743e3e441b7e233f", [:mix], [{:nerves_logging, "~> 0.2.0", [hex: :nerves_logging, repo: "hexpm", optional: false]}, {:nerves_uevent, "~> 0.1.0", [hex: :nerves_uevent, repo: "hexpm", optional: false]}, {:uboot_env, "~> 0.3.0 or ~> 1.0", [hex: :uboot_env, repo: "hexpm", optional: false]}], "hexpm", "6bafb89344709e5405cc7f9b140f91ca76ea3749f0eb3af80d8f8b8c53388b2d"}, 27 | "nerves_ssh": {:hex, :nerves_ssh, "0.4.3", "32540ad52a9781b7b1a1427ea1d282a9129f16b40f0a06de2074019ed455e760", [:mix], [{:nerves_runtime, "~> 0.11", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:ssh_subsystem_fwup, "~> 0.5", [hex: :ssh_subsystem_fwup, repo: "hexpm", optional: false]}], "hexpm", "dfd079e4609d1d231dd29a9588534957a24c0baed1f434233dbfc2a679ea14d8"}, 28 | "nerves_system_bbb": {:hex, :nerves_system_bbb, "2.22.0", "1a48f639ee22a40ec352d6d9f95d0a8eb7ccb4e3710eb333bed381d2eef165cb", [:mix], [{:nerves, "~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv7_nerves_linux_gnueabihf, "~> 13.2.0", [hex: :nerves_toolchain_armv7_nerves_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "5143c7efe75b3d7a2b7c67c08c58612c3df38eb8c04aaab37d0d316cb4458bbb"}, 29 | "nerves_system_br": {:hex, :nerves_system_br, "1.27.0", "95be1ddfe55554d45cd30c2e534a2090691ec94126f3bdc22cd507890adb0e32", [:mix], [], "hexpm", "95a89c478aa6a35cb8baf2554c1840f2057d93bc14cc8d5acce0124c8487c96c"}, 30 | "nerves_system_rpi": {:hex, :nerves_system_rpi, "1.27.0", "a5e51e9079d3ac2ca71cbfaa3e52d7f34adf4ac370a2856b32a67eab62981828", [:mix], [{:nerves, "~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_nerves_linux_gnueabihf, "~> 13.2.0", [hex: :nerves_toolchain_armv6_nerves_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "75bd0f3931e82838114cf3c1494d57fb4641b135b4d45d0c8d702937eec59c5a"}, 31 | "nerves_system_rpi0": {:hex, :nerves_system_rpi0, "1.27.0", "05e34e6b45bcdedb32b21c8c1da7867082b0d69cf960448bcb114f2fdb1611a2", [:mix], [{:nerves, "~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_nerves_linux_gnueabihf, "~> 13.2.0", [hex: :nerves_toolchain_armv6_nerves_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "7d78a0fa74f1e4612ecb18c4009a68478f8fa5351d64e1d346b9094a99eca18c"}, 32 | "nerves_system_rpi2": {:hex, :nerves_system_rpi2, "1.27.0", "16c1bde25eef6af51e3a7bc4fa75f5a4a60e35597e2c20baa95cc9fee53c337c", [:mix], [{:nerves, "~> 1.5.4 or ~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv7_nerves_linux_gnueabihf, "~> 13.2.0", [hex: :nerves_toolchain_armv7_nerves_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "fdd812fba06057255065230adfbbba7bf9b620135b40db2d605f055c7e08b639"}, 33 | "nerves_system_rpi3": {:hex, :nerves_system_rpi3, "1.27.0", "dfdac3789364300d6731804ddae9124d102f372b13786738a4beb3a167177469", [:mix], [{:nerves, "~> 1.5.4 or ~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv7_nerves_linux_gnueabihf, "~> 13.2.0", [hex: :nerves_toolchain_armv7_nerves_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "096d67cbe53d33ce1a07165955faed6c5fc239c5e6e8d0a5e9528b7c51e90bd1"}, 34 | "nerves_system_rpi3a": {:hex, :nerves_system_rpi3a, "1.27.0", "da535610b3d9b546512a408496cdf5f932e8b6b45308dc801c422e67b194cf4a", [:mix], [{:nerves, "~> 1.5.4 or ~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv7_nerves_linux_gnueabihf, "~> 13.2.0", [hex: :nerves_toolchain_armv7_nerves_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "0ea98be718317598906c8c12fc2641a5f4e90bb7792935016eb54553375385a8"}, 35 | "nerves_system_rpi4": {:hex, :nerves_system_rpi4, "1.27.0", "f393cdbb8cbd40dfdae0bc220f5ea3cb239776bae7f8a9ed9ea7c56b62827c5a", [:mix], [{:nerves, "~> 1.5.4 or ~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_aarch64_nerves_linux_gnu, "~> 13.2.0", [hex: :nerves_toolchain_aarch64_nerves_linux_gnu, repo: "hexpm", optional: false]}], "hexpm", "dc2ee32d8829367d291e03f474ab89415c0d836dbbccfbe07e0cb5639f213573"}, 36 | "nerves_system_x86_64": {:hex, :nerves_system_x86_64, "1.27.0", "4f067bcb71f8caec11a35b8f7a2e4dbe2d6852bfc159929a73134afec7eb72fb", [:mix], [{:nerves, "~> 1.5.4 or ~> 1.6.0 or ~> 1.7.15 or ~> 1.8", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.27.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_toolchain_x86_64_nerves_linux_musl, "~> 13.2.0", [hex: :nerves_toolchain_x86_64_nerves_linux_musl, repo: "hexpm", optional: false]}], "hexpm", "7f2a12d105dd98d4af76b6840c691a2e349f6f7ad99167ec8178fd55d66a6e8d"}, 37 | "nerves_time": {:hex, :nerves_time, "0.4.6", "f02e5e866149f3884a4b125104a5b677f7e61d271c1f7d87bc989197dc02dae5", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5 or ~> 1.0", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm", "1fecb9e9dd098c0e93d35205a79e604831779f2d2313cfe7c31d181678e26ce6"}, 38 | "nerves_toolchain_aarch64_nerves_linux_gnu": {:hex, :nerves_toolchain_aarch64_nerves_linux_gnu, "13.2.0", "68fcd2c21c86cceb9545948fae052d72f88b7c7c10205b252dac88559e2a3369", [:mix], [{:nerves, "~> 1.4", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.10.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "f92212606919a062f975e7bd82ed8a1b95bd4864abb3444cd0d5d0e610e94cc5"}, 39 | "nerves_toolchain_armv6_nerves_linux_gnueabihf": {:hex, :nerves_toolchain_armv6_nerves_linux_gnueabihf, "13.2.0", "d7a9a53cc53a99f6bbe13e872b793c84344b5cb2ec5220dc12217cba9a59ddae", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.10.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "97791d351485d4114e992db7814766563892a55c9d22a2ebcc43b3c1247f2b62"}, 40 | "nerves_toolchain_armv7_nerves_linux_gnueabihf": {:hex, :nerves_toolchain_armv7_nerves_linux_gnueabihf, "13.2.0", "48305b5ba2ec41d2f9bfd0997d6bb6e8a9f5358146baa58fc64887bfe2d38ccd", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.10.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "7a43b14eb4ec90f36acf36a42ce42c1d69c280b90eea7ab9965e00db3ee6cbf7"}, 41 | "nerves_toolchain_ctng": {:hex, :nerves_toolchain_ctng, "1.10.0", "c6b35377a0b7a93633a8673a788f1580fe1fa06083374b0e4df36da65828d2ef", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}], "hexpm", "e4ae1a2b84de3502ecac195765819be0ce2834eb276553163a7c03133f1760f1"}, 42 | "nerves_toolchain_x86_64_nerves_linux_musl": {:hex, :nerves_toolchain_x86_64_nerves_linux_musl, "13.2.0", "1896dc1576363e16276164cf350e98493a9b9b9391eafa94315a82adf97d94ea", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.10.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "be6214995b7ee181f9e8ebb4148312775c0a626b857f0bb4688d8efda8844cba"}, 43 | "nerves_uevent": {:hex, :nerves_uevent, "0.1.0", "651111a46be9a238560cbf7946989fc500e5f33d7035fd9ea7194d07a281bc19", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:property_table, "~> 0.2.0", [hex: :property_table, repo: "hexpm", optional: false]}], "hexpm", "cb0b1993c3ed3cefadbcdb534e910af0661f95c3445796ce8a7c8be3519a4e5f"}, 44 | "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, 45 | "nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"}, 46 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 47 | "one_dhcpd": {:hex, :one_dhcpd, "2.0.2", "49ae0bc4ecc4bf958a2e3eb9c25149dbb37102b77163ed3f9ebadfe49090b44a", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f95de030d199c834dacacc8420881e21c27cec60371711bdffd2fc183234cbf8"}, 48 | "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, 49 | "property_table": {:hex, :property_table, "0.2.5", "72567da62711d0b8b517990eadbe76685e5b02ed90ad5a2e48ae6f3a7922cf64", [:mix], [], "hexpm", "abfd4ce4dd9a7a6d39c0a35b177c09dc9aba5af0eb1f93cc28919e31d5f8bbab"}, 50 | "req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"}, 51 | "ring_logger": {:hex, :ring_logger, "0.11.2", "4a6d89d5b9d76253fd73957b56a8a107e6bee89109ccc351b220ab551ea1219e", [:mix], [{:circular_buffer, "~> 0.4.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}], "hexpm", "8f98a8f196ef6f298fc42fdcc4ec0cd678709171e71b0eb3dfca74314aa5c69d"}, 52 | "shoehorn": {:hex, :shoehorn, "0.9.2", "7e430e6f27ba4f6519699e54e8aab52520e658d1a11a5ddf01a1af1602416280", [:mix], [], "hexpm", "044353552341925a930681f66595e5d8ed4748d6e4200b8c877a3859016d1a11"}, 53 | "ssh_subsystem_fwup": {:hex, :ssh_subsystem_fwup, "0.6.1", "628f8e3795de5f1d0e7b3b55de4248ab0a77ab4c47e3cd282f1dda89d6354a9f", [:mix], [], "hexpm", "babdae337f2dc011ab5478662b4ec850650d7acfb165662ae47f6f0ce8892499"}, 54 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 55 | "toolshed": {:hex, :toolshed, "0.3.1", "9c7f90c015e8f6034eb43c4f5203ac6226d0db5f1a575ccf69af94b5d77cba64", [:mix], [{:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: true]}], "hexpm", "92fc4a792cd1dbc4fd6963431b5c3511e88454c68e32a30cf17366905b43612e"}, 56 | "uboot_env": {:hex, :uboot_env, "1.0.1", "b0e136cf1a561412ff7db23ed2b6df18d7c7ce2fc59941afd851006788a67f3d", [:mix], [], "hexpm", "b6d4fe7c24123be57ed946c48116d23173e37944bc945b8b76fccc437909c60b"}, 57 | "vintage_net": {:hex, :vintage_net, "0.13.5", "b02cbb44434eba68d8e991e7c9a75d491da73c3073ff4823843e66f451d6f088", [:make, :mix], [{:beam_notify, "~> 0.2.0 or ~> 1.0", [hex: :beam_notify, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:gen_state_machine, "~> 2.0.0 or ~> 2.1.0 or ~> 3.0.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5.1 or ~> 0.6.0 or ~> 1.0", [hex: :muontrap, repo: "hexpm", optional: false]}, {:property_table, "~> 0.2.0", [hex: :property_table, repo: "hexpm", optional: false]}], "hexpm", "67e04b8eaa40600bbea4902aba363792598b31cf144f6033d93d7662b60519ad"}, 58 | "vintage_net_direct": {:hex, :vintage_net_direct, "0.10.7", "940561c375f04d6734ac78100ae1d8ef790ffd7e966f70efb525230fa1bc5774", [:mix], [{:one_dhcpd, "~> 0.2.3 or ~> 1.0 or ~> 2.0", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.9.1 or ~> 0.10.0 or ~> 0.11.0 or ~> 0.12.0 or ~> 0.13.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm", "c040e9c33220495c28ba4464c5924da00bd4949627c4cb9c99ba1ed96f7b9429"}, 59 | "vintage_net_ethernet": {:hex, :vintage_net_ethernet, "0.11.2", "ef67db5ace9ad5ca5bf229a507247f9eb45b847dc0ff694a6e8a156ed9c5915d", [:mix], [{:vintage_net, "~> 0.12.0 or ~> 0.13.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm", "6915f9e15e1aa15e52d1948f318ce5109181d1ad7aaa50016bad5dd8e22df9ea"}, 60 | "vintage_net_wifi": {:hex, :vintage_net_wifi, "0.12.5", "a19a3f78070470ecc00fa1d3d32982c41f39c2c06edb6cd53a8c13905aed8623", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.12.0 or ~> 0.13.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm", "4b477b9cccf54145bd6f7bf1f023ece63fa11395229bae365861de062c60a2f0"}, 61 | } 62 | -------------------------------------------------------------------------------- /pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xHain-hackspace/xDoor/a682d3d31d6799f5ad6bb774fd8751c61f3507ae/pic1.jpg -------------------------------------------------------------------------------- /pic2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xHain-hackspace/xDoor/a682d3d31d6799f5ad6bb774fd8751c61f3507ae/pic2.jpg -------------------------------------------------------------------------------- /pic3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xHain-hackspace/xDoor/a682d3d31d6799f5ad6bb774fd8751c61f3507ae/pic3.jpg -------------------------------------------------------------------------------- /priv/.gitignore: -------------------------------------------------------------------------------- 1 | authorized_keys* 2 | host_key/* -------------------------------------------------------------------------------- /priv/greeting: -------------------------------------------------------------------------------- 1 | 2 | 3 | _ _ _ 4 | | | | | (_) 5 | __ _| |__| | __ _ _ _ __ 6 | \ \/ / __ |/ _` | | '_ \ 7 | > <| | | | (_| | | | | | 8 | /_/\_\_| |_|\__,_|_|_| |_| 9 | 10 | 11 | -------------------------------------------------------------------------------- /rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Add custom options here 2 | 3 | ## Distributed Erlang Options 4 | ## The cookie needs to be configured prior to vm boot for 5 | ## for read only filesystem. 6 | 7 | -setcookie <%= @release.options[:cookie] %> 8 | 9 | ## Use Ctrl-C to interrupt the current shell rather than invoking the emulator's 10 | ## break handler and possibly exiting the VM. 11 | +Bc 12 | 13 | # Allow time warps so that the Erlang system time can more closely match the 14 | # OS system time. 15 | +C multi_time_warp 16 | 17 | ## Load code at system startup 18 | ## See http://erlang.org/doc/system_principles/system_principles.html#code-loading-strategy 19 | -mode embedded 20 | 21 | ## Save the shell history between reboots 22 | ## See http://erlang.org/doc/man/kernel_app.html for additional options 23 | -kernel shell_history enabled 24 | 25 | ## Enable heartbeat monitoring of the Erlang runtime system 26 | -heart -env HEART_BEAT_TIMEOUT 30 27 | 28 | ## Start the Elixir shell 29 | 30 | -noshell 31 | -user elixir 32 | -run elixir start_iex 33 | 34 | ## Enable colors in the shell 35 | -elixir ansi_enabled true 36 | 37 | ## Options added after -extra are interpreted as plain arguments and can be 38 | ## retrieved using :init.get_plain_arguments(). Options before the "--" are 39 | ## interpreted by Elixir and anything afterwards is left around for other IEx 40 | ## and user applications. 41 | -extra --no-halt 42 | -- 43 | --dot-iex /etc/iex.exs 44 | -------------------------------------------------------------------------------- /rootfs_overlay/etc/iex.exs: -------------------------------------------------------------------------------- 1 | # Add Toolshed helpers to the IEx session 2 | use Toolshed 3 | 4 | NervesMOTD.print() 5 | 6 | # Logger.configure(level: :debug) 7 | 8 | if RingLogger in Application.get_env(:logger, :backends, []) do 9 | RingLogger.tail(250) 10 | RingLogger.attach() 11 | end 12 | -------------------------------------------------------------------------------- /ssh_console.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | host=${1} 4 | 5 | # printf "%s" "waiting for ${host} ..." 6 | # while ! ping -c 1 -n -w 1 ${host} &> /dev/null 7 | # do 8 | # printf "%c" "." 9 | # done 10 | 11 | ssh ${host} -p 23 12 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/xdoor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule XdoorTest do 2 | use ExUnit.Case 3 | doctest Xdoor 4 | 5 | test "greets the world" do 6 | assert Xdoor.hello() == :world 7 | end 8 | end 9 | --------------------------------------------------------------------------------