├── .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 |
--------------------------------------------------------------------------------