diff options
| author | Julian Blake Kongslie | 2024-07-25 13:28:55 -0700 |
|---|---|---|
| committer | Julian Blake Kongslie | 2024-07-25 13:28:55 -0700 |
| commit | 5c49f7c2e46d6fced0763094ec05bba41298cbed (patch) | |
| tree | 951d9fd4744eec31557274e3af659aab01185436 | |
| download | rivulet-main.tar.xz | |
Diffstat (limited to '')
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | CMakeLists.txt | 15 | ||||
| -rw-r--r-- | Makefile | 19 | ||||
| -rwxr-xr-x | read.rb | 56 | ||||
| -rw-r--r-- | rivulet.cpp | 118 | ||||
| l--------- | watering.run | 1 |
6 files changed, 210 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1 @@ | |||
| /build | |||
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f624cc4 --- /dev/null +++ b/CMakeLists.txt | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | cmake_minimum_required(VERSION 3.29) | ||
| 2 | |||
| 3 | if(NOT DEFINED PICO_SDK_PATH) | ||
| 4 | set(PICO_SDK_PATH /usr/src/pico-sdk) | ||
| 5 | endif() | ||
| 6 | |||
| 7 | include(/usr/src/pico-sdk/external/pico_sdk_import.cmake) | ||
| 8 | |||
| 9 | project(rivulet) | ||
| 10 | |||
| 11 | pico_sdk_init() | ||
| 12 | |||
| 13 | add_executable(rivulet rivulet.cpp) | ||
| 14 | target_link_libraries(rivulet hardware_gpio pico_stdio_usb pico_stdlib) | ||
| 15 | pico_add_extra_outputs(rivulet) | ||
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..22d8da7 --- /dev/null +++ b/Makefile | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | connect: | ||
| 2 | picocom --baud 115200 --flow none --parity none --databits 8 --stopbits 1 --nolock --echo --raise-rts --raise-dtr /dev/ttyACM1 | ||
| 3 | .PHONY: connect | ||
| 4 | |||
| 5 | build: | ||
| 6 | mkdir -p build | ||
| 7 | cmake -B build -G Ninja | ||
| 8 | cmake --build build | ||
| 9 | .PHONY: build | ||
| 10 | |||
| 11 | install: | ||
| 12 | $(MAKE) build | ||
| 13 | picotool load -f -x build/rivulet.uf2 | ||
| 14 | @until [ -e /dev/ttyACM1 ]; do sleep 0.1; done; sleep 1 | ||
| 15 | .PHONY: install | ||
| 16 | |||
| 17 | clean: | ||
| 18 | rm -rf build | ||
| 19 | .PHONY: clean | ||
| @@ -0,0 +1,56 @@ | |||
| 1 | #!/usr/bin/ruby -w | ||
| 2 | |||
| 3 | require "enumerable/statistics" | ||
| 4 | require "serialport" | ||
| 5 | |||
| 6 | SENSOR_BITS = 12 | ||
| 7 | SENSOR_MAX = (1 << SENSOR_BITS) - 1 | ||
| 8 | |||
| 9 | $stdout.sync = true | ||
| 10 | def log(msg) | ||
| 11 | $stdout.write("#{Time.now.strftime("%H:%M:%S")} #{msg}\n") | ||
| 12 | $stdout.flush | ||
| 13 | end | ||
| 14 | |||
| 15 | sensors = [] | ||
| 16 | |||
| 17 | SerialPort.open("/dev/ttyACM1", 115200, 8, 1, SerialPort::NONE) do | port | | ||
| 18 | |||
| 19 | log("Allowing sensors to settle...") | ||
| 20 | |||
| 21 | sleep(3) | ||
| 22 | begin | ||
| 23 | port.read_nonblock(16 << 20) | ||
| 24 | rescue IO::WaitReadable | ||
| 25 | end | ||
| 26 | port.readline | ||
| 27 | |||
| 28 | log("Reading soil moisture sensors...") | ||
| 29 | |||
| 30 | 10.times do | ||
| 31 | raw = port.readline | ||
| 32 | raise "Cannot parse sensor results #{raw.inspect}" unless raw =~ /\[([^\]]+)\]/ | ||
| 33 | |||
| 34 | sensors << $1.split(/\s+/).filter { | x | not x.empty? }.map { | x | x.to_i } | ||
| 35 | |||
| 36 | log("Raw sensor results: \t#{sensors[-1].join("\t")}") | ||
| 37 | end | ||
| 38 | |||
| 39 | end | ||
| 40 | |||
| 41 | avgsensors = [] | ||
| 42 | stdsensors = [] | ||
| 43 | |||
| 44 | sensors.transpose.each do | sensor | | ||
| 45 | avgsensors << sensor.mean.round | ||
| 46 | stdsensors << sensor.stdev.round | ||
| 47 | end | ||
| 48 | |||
| 49 | log("Avg sensor results: \t#{avgsensors.join("\t")} \t(per sensor)") | ||
| 50 | log("SDv sensor results: \t#{stdsensors.join("\t")} \t(per sensor)") | ||
| 51 | |||
| 52 | avg = sensors.flatten.mean.round | ||
| 53 | std = sensors.flatten.stdev.round | ||
| 54 | |||
| 55 | log("Avg sensor results: \t#{avg} \t(#{(100.0 * avg.to_f / SENSOR_MAX.to_f).round}%) \t(overall)") | ||
| 56 | log("SDv sensor results: \t#{std} \t(overall)") | ||
diff --git a/rivulet.cpp b/rivulet.cpp new file mode 100644 index 0000000..5b18bb0 --- /dev/null +++ b/rivulet.cpp | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | #include <hardware/gpio.h> | ||
| 2 | #include <pico/printf.h> | ||
| 3 | #include <pico/stdio_usb.h> | ||
| 4 | #include <pico/stdlib.h> | ||
| 5 | |||
| 6 | constexpr const unsigned int PERIOD_MS = 3000; | ||
| 7 | |||
| 8 | enum { | ||
| 9 | PIN_CLOCK, | ||
| 10 | |||
| 11 | PIN_DATA, | ||
| 12 | |||
| 13 | PIN_CS0, | ||
| 14 | PIN_CS1, | ||
| 15 | PIN_CS2, | ||
| 16 | PIN_CS3, | ||
| 17 | PIN_CS4, | ||
| 18 | PIN_CS5, | ||
| 19 | PIN_CS6, | ||
| 20 | PIN_CS7, | ||
| 21 | |||
| 22 | PIN_LED = 25, | ||
| 23 | }; | ||
| 24 | |||
| 25 | constexpr unsigned int MASK(unsigned int pin) { return 1 << pin; } | ||
| 26 | |||
| 27 | enum { | ||
| 28 | MASK_CLOCK = MASK(PIN_CLOCK), | ||
| 29 | |||
| 30 | MASK_DATA = MASK(PIN_DATA), | ||
| 31 | |||
| 32 | MASK_CS0 = MASK(PIN_CS0), | ||
| 33 | MASK_CS1 = MASK(PIN_CS1), | ||
| 34 | MASK_CS2 = MASK(PIN_CS2), | ||
| 35 | MASK_CS3 = MASK(PIN_CS3), | ||
| 36 | MASK_CS4 = MASK(PIN_CS4), | ||
| 37 | MASK_CS5 = MASK(PIN_CS5), | ||
| 38 | MASK_CS6 = MASK(PIN_CS6), | ||
| 39 | MASK_CS7 = MASK(PIN_CS7), | ||
| 40 | |||
| 41 | MASK_LED = MASK(PIN_LED), | ||
| 42 | |||
| 43 | MASK_CSALL = MASK_CS0 | MASK_CS1 | MASK_CS2 | MASK_CS3 | MASK_CS4 | MASK_CS5 | MASK_CS6 | MASK_CS7, | ||
| 44 | MASK_IN = MASK_DATA, | ||
| 45 | MASK_OUT = MASK_CLOCK | MASK_CSALL | MASK_LED, | ||
| 46 | MASK_ALL = MASK_IN | MASK_OUT, | ||
| 47 | }; | ||
| 48 | |||
| 49 | unsigned int read_bit() { | ||
| 50 | gpio_set_mask(MASK_CLOCK); | ||
| 51 | sleep_us(1); | ||
| 52 | gpio_clr_mask(MASK_CLOCK); | ||
| 53 | auto bit = gpio_get(PIN_DATA); | ||
| 54 | sleep_us(1); | ||
| 55 | return bit; | ||
| 56 | } | ||
| 57 | |||
| 58 | unsigned int read_sensor(unsigned int i) { | ||
| 59 | unsigned int cs; | ||
| 60 | switch (i) { | ||
| 61 | case 0: cs = PIN_CS0; break; | ||
| 62 | case 1: cs = PIN_CS1; break; | ||
| 63 | case 2: cs = PIN_CS2; break; | ||
| 64 | case 3: cs = PIN_CS3; break; | ||
| 65 | case 4: cs = PIN_CS4; break; | ||
| 66 | case 5: cs = PIN_CS5; break; | ||
| 67 | case 6: cs = PIN_CS6; break; | ||
| 68 | case 7: cs = PIN_CS7; break; | ||
| 69 | default: return 0; | ||
| 70 | } | ||
| 71 | |||
| 72 | gpio_clr_mask(MASK(cs)); | ||
| 73 | sleep_us(1); | ||
| 74 | |||
| 75 | // 3 leading zero bits | ||
| 76 | for (unsigned int i = 0; i < 3; ++i) | ||
| 77 | read_bit(); | ||
| 78 | |||
| 79 | // 12 data bits; big endian | ||
| 80 | unsigned int result = 0; | ||
| 81 | for (unsigned int i = 0; i < 12; ++i) | ||
| 82 | result = (result << 1) | read_bit(); | ||
| 83 | |||
| 84 | gpio_set_mask(MASK(cs)); | ||
| 85 | sleep_us(1); | ||
| 86 | |||
| 87 | return result; | ||
| 88 | } | ||
| 89 | |||
| 90 | int main() { | ||
| 91 | stdio_usb_init(); | ||
| 92 | gpio_init_mask(MASK_ALL); | ||
| 93 | gpio_set_dir_in_masked(MASK_IN); | ||
| 94 | gpio_set_dir_out_masked(MASK_OUT); | ||
| 95 | gpio_set_mask(MASK_CSALL); | ||
| 96 | gpio_clr_mask(MASK_CLOCK | MASK_LED); | ||
| 97 | |||
| 98 | while (true) { | ||
| 99 | if (!stdio_usb_connected()) { | ||
| 100 | sleep_ms(1000); | ||
| 101 | continue; | ||
| 102 | } | ||
| 103 | |||
| 104 | gpio_set_mask(MASK_LED); | ||
| 105 | |||
| 106 | while (stdio_usb_connected()) { | ||
| 107 | printf("["); | ||
| 108 | for (unsigned int i = 0; i < 8; ++i) | ||
| 109 | printf(" %u", read_sensor(i)); | ||
| 110 | printf(" ]\n"); | ||
| 111 | sleep_ms(PERIOD_MS); | ||
| 112 | } | ||
| 113 | |||
| 114 | gpio_clr_mask(MASK_LED); | ||
| 115 | } | ||
| 116 | |||
| 117 | return 0; | ||
| 118 | } | ||
diff --git a/watering.run b/watering.run new file mode 120000 index 0000000..ef7c810 --- /dev/null +++ b/watering.run | |||
| @@ -0,0 +1 @@ | |||
| /var/lib/laminar/cfg/jobs/watering.run \ No newline at end of file | |||
