diff --git a/.gitignore b/.gitignore index 482690d..64d5517 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,6 @@ test/images/ test_failures.csv false_positives.csv runs/ +recordings/ +.DS_Store +.config \ No newline at end of file diff --git a/README.md b/README.md index 42743b2..df4de77 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,18 @@ If you want to run Waspinator directly on a Raspberry Pi 5 (in [your own 3D prin sudo apt install python3-picamera2 ``` -2. **Set up a Python virtual environment (recommended):** +2. **Make changes to config** + ```bash + sudo nano /boot/firmware/config.txt + + #add the following line to the file somewhere at the top + dtoverlay=pwm-2chan + + #restart your device + sudo reboot + ``` + +3. **Set up a Python virtual environment (recommended):** ```bash # Create virtual environment python -m venv .venv --system-site-packages @@ -60,12 +71,19 @@ If you want to run Waspinator directly on a Raspberry Pi 5 (in [your own 3D prin source .venv/bin/activate ``` -3. **Install Python dependencies:** +4. **Install Python dependencies:** ```bash pip install --upgrade pip pip install -r requirements.txt ``` +5. **Run Setup to move servo to initial position** + ```bash + python -m waspinator setup + ``` + Once this ran, you can screw in the motor lever to the servo and make sure that the trap is in the OPEN state. + After that you can mount the camera and close up the entire trap. it should be ready now. + The Pi Camera module is supported via `python3-picamera2` so you can use the camera as a video source for wasp detection and trapping. Make sure your camera is enabled and properly connected. diff --git a/add-wifi.sh b/add-wifi.sh new file mode 100755 index 0000000..e708879 --- /dev/null +++ b/add-wifi.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +if [ "$EUID" -ne 0 ]; then + echo "Run with sudo" + exit 1 +fi + +if [ $# -lt 2 ]; then + echo "Usage: add-wifi [priority]" + exit 1 +fi + +SSID="$1" +PSK="$2" +PRIORITY="${3:-5}" + +nmcli con add type wifi ifname wlan0 con-name "$SSID" ssid "$SSID" \ + wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$PSK" + +nmcli con mod "$SSID" connection.autoconnect-priority "$PRIORITY" + +echo "Added $SSID with priority $PRIORITY" + diff --git a/hardware/trap_v2.2 - bottom.stl b/hardware/trap_v2.2 - bottom.stl new file mode 100644 index 0000000..5f6c710 Binary files /dev/null and b/hardware/trap_v2.2 - bottom.stl differ diff --git a/hardware/trap_v2.2 - catcher.stl b/hardware/trap_v2.2 - catcher.stl new file mode 100644 index 0000000..0420b56 Binary files /dev/null and b/hardware/trap_v2.2 - catcher.stl differ diff --git a/hardware/trap_v2.2 - electro_cover.stl b/hardware/trap_v2.2 - electro_cover.stl new file mode 100644 index 0000000..c032471 Binary files /dev/null and b/hardware/trap_v2.2 - electro_cover.stl differ diff --git a/hardware/trap_v2.2 - inner.stl b/hardware/trap_v2.2 - inner.stl new file mode 100644 index 0000000..bd6f3b7 Binary files /dev/null and b/hardware/trap_v2.2 - inner.stl differ diff --git a/hardware/trap_v2.2 - outer.stl b/hardware/trap_v2.2 - outer.stl new file mode 100644 index 0000000..9ac57ca Binary files /dev/null and b/hardware/trap_v2.2 - outer.stl differ diff --git a/hardware/trap_v2.2 - servo_adapter.stl b/hardware/trap_v2.2 - servo_adapter.stl new file mode 100644 index 0000000..c939314 Binary files /dev/null and b/hardware/trap_v2.2 - servo_adapter.stl differ diff --git a/models/yolo26n-waspinator-chamber_ncnn_model/metadata.yaml b/models/yolo26n-waspinator-chamber_ncnn_model/metadata.yaml new file mode 100644 index 0000000..88b9c31 --- /dev/null +++ b/models/yolo26n-waspinator-chamber_ncnn_model/metadata.yaml @@ -0,0 +1,20 @@ +description: Ultralytics YOLO26n model trained on /kaggle/working/config.yaml +author: Ultralytics +date: '2026-03-06T10:16:59.662248' +version: 8.4.14 +license: AGPL-3.0 License (https://ultralytics.com/license) +docs: https://docs.ultralytics.com +stride: 32 +task: detect +batch: 1 +imgsz: +- 640 +- 640 +names: + 0: Vespa_velutina + 1: Vespa_crabro +args: + batch: 1 + half: false +channels: 3 +end2end: false diff --git a/models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.bin b/models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.bin new file mode 100644 index 0000000..19b5fb5 Binary files /dev/null and b/models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.bin differ diff --git a/models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.param b/models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.param new file mode 100644 index 0000000..1776e3b --- /dev/null +++ b/models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.param @@ -0,0 +1,322 @@ +7767517 +320 382 +Input in0 0 1 in0 +Convolution conv_3 1 1 in0 1 0=16 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=432 +Swish silu_100 1 1 1 2 +Convolution conv_4 1 1 2 3 0=32 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=4608 +Swish silu_101 1 1 3 4 +Convolution conv_5 1 1 4 5 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=1024 +Swish silu_102 1 1 5 6 +Slice split_0 1 2 6 7 8 -23300=2,16,16 1=0 +Split splitncnn_0 1 3 8 9 10 11 +Convolution conv_6 1 1 11 12 0=8 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152 +Swish silu_103 1 1 12 13 +Convolution conv_7 1 1 13 14 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152 +Swish silu_104 1 1 14 15 +BinaryOp add_0 2 1 10 15 16 0=0 +Concat cat_0 3 1 7 9 16 17 0=0 +Convolution conv_8 1 1 17 18 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=3072 +Swish silu_105 1 1 18 19 +Convolution conv_9 1 1 19 20 0=64 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=36864 +Swish silu_106 1 1 20 21 +Convolution conv_10 1 1 21 22 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_107 1 1 22 23 +Slice split_1 1 2 23 24 25 -23300=2,32,32 1=0 +Split splitncnn_1 1 3 25 26 27 28 +Convolution conv_11 1 1 28 29 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=4608 +Swish silu_108 1 1 29 30 +Convolution conv_12 1 1 30 31 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=4608 +Swish silu_109 1 1 31 32 +BinaryOp add_1 2 1 27 32 33 0=0 +Concat cat_1 3 1 24 26 33 34 0=0 +Convolution conv_13 1 1 34 35 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=12288 +Swish silu_110 1 1 35 36 +Split splitncnn_2 1 2 36 37 38 +Convolution conv_14 1 1 38 39 0=128 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=147456 +Swish silu_111 1 1 39 40 +Convolution conv_15 1 1 40 41 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384 +Swish silu_112 1 1 41 42 +Slice split_2 1 2 42 43 44 -23300=2,64,64 1=0 +Split splitncnn_3 1 3 44 45 46 47 +Convolution conv_16 1 1 47 48 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=2048 +Swish silu_113 1 1 48 49 +Split splitncnn_4 1 2 49 50 51 +Convolution conv_17 1 1 51 52 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_114 1 1 52 53 +Convolution conv_18 1 1 53 54 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_115 1 1 54 55 +BinaryOp add_2 2 1 50 55 56 0=0 +Split splitncnn_5 1 2 56 57 58 +Convolution conv_19 1 1 58 59 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_116 1 1 59 60 +Convolution conv_20 1 1 60 61 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_117 1 1 61 62 +BinaryOp add_3 2 1 57 62 63 0=0 +Convolution conv_21 1 1 46 64 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=2048 +Swish silu_118 1 1 64 65 +Concat cat_2 2 1 63 65 66 0=0 +Convolution conv_22 1 1 66 67 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_119 1 1 67 68 +Concat cat_3 3 1 43 45 68 69 0=0 +Convolution conv_23 1 1 69 70 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=24576 +Swish silu_120 1 1 70 71 +Split splitncnn_6 1 2 71 72 73 +Convolution conv_24 1 1 73 74 0=256 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=294912 +Swish silu_121 1 1 74 75 +Convolution conv_25 1 1 75 76 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=65536 +Swish silu_122 1 1 76 77 +Slice split_3 1 2 77 78 79 -23300=2,128,128 1=0 +Split splitncnn_7 1 3 79 80 81 82 +Convolution conv_26 1 1 82 83 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=8192 +Swish silu_123 1 1 83 84 +Split splitncnn_8 1 2 84 85 86 +Convolution conv_27 1 1 86 87 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864 +Swish silu_124 1 1 87 88 +Convolution conv_28 1 1 88 89 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864 +Swish silu_125 1 1 89 90 +BinaryOp add_4 2 1 85 90 91 0=0 +Split splitncnn_9 1 2 91 92 93 +Convolution conv_29 1 1 93 94 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864 +Swish silu_126 1 1 94 95 +Convolution conv_30 1 1 95 96 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864 +Swish silu_127 1 1 96 97 +BinaryOp add_5 2 1 92 97 98 0=0 +Convolution conv_31 1 1 81 99 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=8192 +Swish silu_128 1 1 99 100 +Concat cat_4 2 1 98 100 101 0=0 +Convolution conv_32 1 1 101 102 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384 +Swish silu_129 1 1 102 103 +Concat cat_5 3 1 78 80 103 104 0=0 +Convolution conv_33 1 1 104 105 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=98304 +Swish silu_130 1 1 105 106 +Split splitncnn_10 1 2 106 107 108 +Convolution conv_34 1 1 108 109 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=32768 +Split splitncnn_11 1 2 109 110 111 +Pooling maxpool2d_97 1 1 111 112 0=0 1=5 11=5 12=1 13=2 2=1 3=2 5=1 +Split splitncnn_12 1 2 112 113 114 +Pooling maxpool2d_98 1 1 114 115 0=0 1=5 11=5 12=1 13=2 2=1 3=2 5=1 +Split splitncnn_13 1 2 115 116 117 +Pooling maxpool2d_99 1 1 117 118 0=0 1=5 11=5 12=1 13=2 2=1 3=2 5=1 +Concat cat_6 4 1 110 113 116 118 119 0=0 +Convolution conv_35 1 1 119 120 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=131072 +Swish silu_131 1 1 120 121 +BinaryOp add_6 2 1 121 107 122 0=0 +Convolution conv_36 1 1 122 123 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=65536 +Swish silu_132 1 1 123 124 +Slice split_4 1 2 124 125 126 -23300=2,128,128 1=0 +Split splitncnn_14 1 2 126 127 128 +Convolution conv_37 1 1 128 129 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=32768 +Reshape reshape_189 1 1 129 130 0=400 1=128 2=2 +Slice split_5 1 3 130 131 132 133 -23300=3,32,32,64 1=1 +Split splitncnn_15 1 2 133 134 135 +Permute transpose_206 1 1 131 136 0=1 +MatMul matmul_202 2 1 136 132 137 +BinaryOp mul_7 1 1 137 138 0=2 1=1 2=0.176777 +Softmax softmax_1 1 1 138 139 0=2 1=1 +MatMul matmultransb_0 2 1 135 139 140 0=1 +Reshape reshape_190 1 1 140 141 0=20 1=20 2=128 +Reshape reshape_191 1 1 134 142 0=20 1=20 2=128 +ConvolutionDepthWise convdw_210 1 1 142 143 0=128 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152 7=128 +BinaryOp add_8 2 1 141 143 144 0=0 +Convolution conv_38 1 1 144 145 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384 +BinaryOp add_9 2 1 127 145 146 0=0 +Split splitncnn_16 1 2 146 147 148 +Convolution conv_39 1 1 148 149 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=32768 +Swish silu_133 1 1 149 150 +Convolution conv_40 1 1 150 151 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=32768 +BinaryOp add_10 2 1 147 151 152 0=0 +Concat cat_7 2 1 125 152 153 0=0 +Convolution conv_41 1 1 153 154 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=65536 +Swish silu_134 1 1 154 155 +Split splitncnn_17 1 2 155 156 157 +Interp upsample_187 1 1 157 158 0=1 1=2.0 2=2.0 6=0 +Concat cat_8 2 1 158 72 159 0=0 +Convolution conv_42 1 1 159 160 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=49152 +Swish silu_135 1 1 160 161 +Slice split_6 1 2 161 162 163 -23300=2,64,64 1=0 +Split splitncnn_18 1 3 163 164 165 166 +Convolution conv_43 1 1 166 167 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=2048 +Swish silu_136 1 1 167 168 +Split splitncnn_19 1 2 168 169 170 +Convolution conv_44 1 1 170 171 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_137 1 1 171 172 +Convolution conv_45 1 1 172 173 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_138 1 1 173 174 +BinaryOp add_11 2 1 169 174 175 0=0 +Split splitncnn_20 1 2 175 176 177 +Convolution conv_46 1 1 177 178 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_139 1 1 178 179 +Convolution conv_47 1 1 179 180 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_140 1 1 180 181 +BinaryOp add_12 2 1 176 181 182 0=0 +Convolution conv_48 1 1 165 183 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=2048 +Swish silu_141 1 1 183 184 +Concat cat_9 2 1 182 184 185 0=0 +Convolution conv_49 1 1 185 186 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_142 1 1 186 187 +Concat cat_10 3 1 162 164 187 188 0=0 +Convolution conv_50 1 1 188 189 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=24576 +Swish silu_143 1 1 189 190 +Split splitncnn_21 1 2 190 191 192 +Interp upsample_188 1 1 192 193 0=1 1=2.0 2=2.0 6=0 +Concat cat_11 2 1 193 37 194 0=0 +Convolution conv_51 1 1 194 195 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384 +Swish silu_144 1 1 195 196 +Slice split_7 1 2 196 197 198 -23300=2,32,32 1=0 +Split splitncnn_22 1 3 198 199 200 201 +Convolution conv_52 1 1 201 202 0=16 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=512 +Swish silu_145 1 1 202 203 +Split splitncnn_23 1 2 203 204 205 +Convolution conv_53 1 1 205 206 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 +Swish silu_146 1 1 206 207 +Convolution conv_54 1 1 207 208 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 +Swish silu_147 1 1 208 209 +BinaryOp add_13 2 1 204 209 210 0=0 +Split splitncnn_24 1 2 210 211 212 +Convolution conv_55 1 1 212 213 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 +Swish silu_148 1 1 213 214 +Convolution conv_56 1 1 214 215 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 +Swish silu_149 1 1 215 216 +BinaryOp add_14 2 1 211 216 217 0=0 +Convolution conv_57 1 1 200 218 0=16 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=512 +Swish silu_150 1 1 218 219 +Concat cat_12 2 1 217 219 220 0=0 +Convolution conv_58 1 1 220 221 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=1024 +Swish silu_151 1 1 221 222 +Concat cat_13 3 1 197 199 222 223 0=0 +Convolution conv_59 1 1 223 224 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=6144 +Swish silu_152 1 1 224 225 +Split splitncnn_25 1 3 225 226 227 228 +Convolution conv_60 1 1 227 229 0=64 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=36864 +Swish silu_153 1 1 229 230 +Concat cat_14 2 1 230 191 231 0=0 +Convolution conv_61 1 1 231 232 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=24576 +Swish silu_154 1 1 232 233 +Slice split_8 1 2 233 234 235 -23300=2,64,64 1=0 +Split splitncnn_26 1 3 235 236 237 238 +Convolution conv_62 1 1 238 239 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=2048 +Swish silu_155 1 1 239 240 +Split splitncnn_27 1 2 240 241 242 +Convolution conv_63 1 1 242 243 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_156 1 1 243 244 +Convolution conv_64 1 1 244 245 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_157 1 1 245 246 +BinaryOp add_15 2 1 241 246 247 0=0 +Split splitncnn_28 1 2 247 248 249 +Convolution conv_65 1 1 249 250 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_158 1 1 250 251 +Convolution conv_66 1 1 251 252 0=32 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_159 1 1 252 253 +BinaryOp add_16 2 1 248 253 254 0=0 +Convolution conv_67 1 1 237 255 0=32 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=2048 +Swish silu_160 1 1 255 256 +Concat cat_15 2 1 254 256 257 0=0 +Convolution conv_68 1 1 257 258 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_161 1 1 258 259 +Concat cat_16 3 1 234 236 259 260 0=0 +Convolution conv_69 1 1 260 261 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=24576 +Swish silu_162 1 1 261 262 +Split splitncnn_29 1 3 262 263 264 265 +Convolution conv_70 1 1 264 266 0=128 1=3 11=3 12=1 13=2 14=1 2=1 3=2 4=1 5=1 6=147456 +Swish silu_163 1 1 266 267 +Concat cat_17 2 1 267 156 268 0=0 +Convolution conv_71 1 1 268 269 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=98304 +Swish silu_164 1 1 269 270 +Slice split_9 1 2 270 271 272 -23300=2,128,128 1=0 +Split splitncnn_30 1 3 272 273 274 275 +Convolution conv_72 1 1 275 276 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=73728 +Swish silu_165 1 1 276 277 +Convolution conv_73 1 1 277 278 0=128 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=73728 +Swish silu_166 1 1 278 279 +BinaryOp add_17 2 1 274 279 280 0=0 +Split splitncnn_31 1 2 280 281 282 +Convolution conv_74 1 1 282 283 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=32768 +Reshape reshape_192 1 1 283 284 0=400 1=128 2=2 +Slice split_10 1 3 284 285 286 287 -23300=3,32,32,64 1=1 +Split splitncnn_32 1 2 287 288 289 +Permute transpose_208 1 1 285 290 0=1 +MatMul matmul_204 2 1 290 286 291 +BinaryOp mul_18 1 1 291 292 0=2 1=1 2=0.176777 +Softmax softmax_2 1 1 292 293 0=2 1=1 +MatMul matmultransb_1 2 1 289 293 294 0=1 +Reshape reshape_193 1 1 294 295 0=20 1=20 2=128 +Reshape reshape_194 1 1 288 296 0=20 1=20 2=128 +ConvolutionDepthWise convdw_211 1 1 296 297 0=128 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152 7=128 +BinaryOp add_19 2 1 295 297 298 0=0 +Convolution conv_75 1 1 298 299 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384 +BinaryOp add_20 2 1 281 299 300 0=0 +Split splitncnn_33 1 2 300 301 302 +Convolution conv_76 1 1 302 303 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=32768 +Swish silu_167 1 1 303 304 +Convolution conv_77 1 1 304 305 0=128 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=32768 +BinaryOp add_21 2 1 301 305 306 0=0 +Concat cat_18 3 1 271 273 306 307 0=0 +Convolution conv_78 1 1 307 308 0=256 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=98304 +Swish silu_168 1 1 308 309 +Split splitncnn_34 1 2 309 310 311 +MemoryData pnnx_262 0 1 312 0=8400 +Convolution conv_79 1 1 226 313 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=9216 +Swish silu_169 1 1 313 314 +Convolution conv_80 1 1 314 315 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 +Swish silu_170 1 1 315 316 +Convolution conv_81 1 1 316 317 0=4 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=64 +Reshape reshape_195 1 1 317 318 0=6400 1=4 +Convolution conv_82 1 1 263 319 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=18432 +Swish silu_171 1 1 319 320 +Convolution conv_83 1 1 320 321 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 +Swish silu_172 1 1 321 322 +Convolution conv_84 1 1 322 323 0=4 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=64 +Reshape reshape_196 1 1 323 324 0=1600 1=4 +Convolution conv_85 1 1 310 325 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=36864 +Swish silu_173 1 1 325 326 +Convolution conv_86 1 1 326 327 0=16 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 +Swish silu_174 1 1 327 328 +Convolution conv_87 1 1 328 329 0=4 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=64 +Reshape reshape_197 1 1 329 330 0=400 1=4 +Concat cat_19 3 1 318 324 330 331 0=1 +ConvolutionDepthWise convdw_212 1 1 228 332 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64 +Swish silu_175 1 1 332 333 +Convolution conv_88 1 1 333 334 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_176 1 1 334 335 +ConvolutionDepthWise convdw_213 1 1 335 336 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64 +Swish silu_177 1 1 336 337 +Convolution conv_89 1 1 337 338 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_178 1 1 338 339 +Convolution conv_90 1 1 339 340 0=2 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=128 +Reshape reshape_198 1 1 340 341 0=6400 1=2 +ConvolutionDepthWise convdw_214 1 1 265 342 0=128 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=1152 7=128 +Swish silu_179 1 1 342 343 +Convolution conv_91 1 1 343 344 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=8192 +Swish silu_180 1 1 344 345 +ConvolutionDepthWise convdw_215 1 1 345 346 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64 +Swish silu_181 1 1 346 347 +Convolution conv_92 1 1 347 348 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_182 1 1 348 349 +Convolution conv_93 1 1 349 350 0=2 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=128 +Reshape reshape_199 1 1 350 351 0=1600 1=2 +ConvolutionDepthWise convdw_216 1 1 311 352 0=256 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=2304 7=256 +Swish silu_183 1 1 352 353 +Convolution conv_94 1 1 353 354 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=16384 +Swish silu_184 1 1 354 355 +ConvolutionDepthWise convdw_217 1 1 355 356 0=64 1=3 11=3 12=1 13=1 14=1 2=1 3=1 4=1 5=1 6=576 7=64 +Swish silu_185 1 1 356 357 +Convolution conv_95 1 1 357 358 0=64 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=4096 +Swish silu_186 1 1 358 359 +Convolution conv_96 1 1 359 360 0=2 1=1 11=1 12=1 13=1 14=0 2=1 3=1 4=0 5=1 6=128 +Reshape reshape_200 1 1 360 361 0=400 1=2 +Concat cat_20 3 1 341 351 361 362 0=1 +MemoryData pnnx_fold_anchor_points.1 0 1 363 0=8400 1=2 +Split splitncnn_0 1 2 363 364 365 +Slice chunk_0 1 2 331 366 367 -23300=2,-233,-233 1=0 +BinaryOp sub_22 2 1 364 366 368 0=1 +Split splitncnn_35 1 2 368 369 370 +BinaryOp add_23 2 1 365 367 371 0=0 +Split splitncnn_36 1 2 371 372 373 +BinaryOp add_24 2 1 369 372 374 0=0 +BinaryOp div_25 1 1 374 375 0=3 1=1 2=2.0 +BinaryOp sub_26 2 1 373 370 376 0=1 +Concat cat_21 2 1 375 376 377 0=0 +Reshape reshape_201 1 1 312 378 0=8400 1=1 +BinaryOp mul_27 2 1 377 378 379 0=2 +Sigmoid sigmoid_0 1 1 362 380 +Concat cat_22 2 1 379 380 out0 0=0 diff --git a/models/yolo26n-waspinator-chamber_ncnn_model/model_ncnn.py b/models/yolo26n-waspinator-chamber_ncnn_model/model_ncnn.py new file mode 100644 index 0000000..e556ad0 --- /dev/null +++ b/models/yolo26n-waspinator-chamber_ncnn_model/model_ncnn.py @@ -0,0 +1,26 @@ +import numpy as np +import ncnn +import torch + +def test_inference(): + torch.manual_seed(0) + in0 = torch.rand(1, 3, 640, 640, dtype=torch.float) + out = [] + + with ncnn.Net() as net: + net.load_param("models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.param") + net.load_model("models/yolo26n-waspinator-chamber_ncnn_model/model.ncnn.bin") + + with net.create_extractor() as ex: + ex.input("in0", ncnn.Mat(in0.squeeze(0).numpy()).clone()) + + _, out0 = ex.extract("out0") + out.append(torch.from_numpy(np.array(out0)).unsqueeze(0)) + + if len(out) == 1: + return out[0] + else: + return tuple(out) + +if __name__ == "__main__": + print(test_inference()) diff --git a/requirements.txt b/requirements.txt index 5903d86..a995c32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,6 @@ flask>=3.1.2 onnx>=1.20.0 screeninfo pytest +rpi-hardware-pwm +ncnn==1.0.20260114 +dotenv diff --git a/waspinator/.config_default b/waspinator/.config_default new file mode 100644 index 0000000..17c979b --- /dev/null +++ b/waspinator/.config_default @@ -0,0 +1,2 @@ +SERVO_OPEN=8.2 +SERVO_CLOSED=4.6 \ No newline at end of file diff --git a/waspinator/decider.py b/waspinator/decider.py index 80574a9..52c870e 100644 --- a/waspinator/decider.py +++ b/waspinator/decider.py @@ -1,9 +1,13 @@ from collections import deque +from waspinator.patience_countdown import PatienceCountdown from waspinator.trap import TrapCommand, TrapState VELUTINA = "Vespa_velutina" -def decide(current_state: TrapState, summary_history: deque[list[dict]], is_trap_ready: bool) -> tuple[TrapCommand, TrapState]: +def decide(current_state: TrapState, summary_history: deque[list[dict]], is_trap_ready: bool, patience: PatienceCountdown) -> tuple[TrapCommand, TrapState]: + if len(summary_history) < summary_history.maxlen: + # we don't have enough history yet to make a decision, so we wait + return (TrapCommand.NO_OP, current_state) if not is_trap_ready: # Trap is not ready yet; we wait @@ -11,14 +15,23 @@ def decide(current_state: TrapState, summary_history: deque[list[dict]], is_trap if current_state == TrapState.READY_TO_TRIGGER: every_summary_has_vespa_velutina = all(any(d.get("name") == VELUTINA for d in summary) for summary in summary_history) - anything_else_detected = any(any(d.get("name") != VELUTINA for d in frame) for frame in summary_history) + anything_else_detected = any(any(d.get("name") != VELUTINA for d in frame) for frame in summary_history) # with the current model this can only be a crabro, other insects would not trigger a detection if not anything_else_detected and every_summary_has_vespa_velutina: + patience.reset() return (TrapCommand.TRIGGER, TrapState.WAITING_FOR_CLEARANCE) - return (TrapCommand.NO_OP, current_state) + + # we still haven't detected a velutina: our patience is running thin + patience.tick() + if patience.ran_out(): + patience.reset() + return (TrapCommand.SLEEP, TrapState.READY_TO_TRIGGER) # we're going back to sleep + elif current_state == TrapState.WAITING_FOR_CLEARANCE: any_velutina_detected = any(any(d.get("name") == VELUTINA for d in summary) for summary in summary_history) if not any_velutina_detected: return (TrapCommand.RESET, TrapState.READY_TO_TRIGGER) - return (TrapCommand.NO_OP, current_state) + return (TrapCommand.WAIT, current_state) # we wait until the trap is clear of velutina before we can trigger again + + return (TrapCommand.NO_OP, current_state) diff --git a/waspinator/event_recorder.py b/waspinator/event_recorder.py index 4d74854..aa5d69d 100644 --- a/waspinator/event_recorder.py +++ b/waspinator/event_recorder.py @@ -1,11 +1,14 @@ from datetime import datetime +import logging import os -import time +import shutil import cv2 as cv import numpy as np +logger = logging.getLogger(__name__) + class EventRecorder: - def __init__(self, output_dir: str, img_size: tuple[int, int], fps: float = 30.0): + def __init__(self, output_dir: str, img_size: tuple[int, int], fps: float = 15.0): self.output_dir = output_dir self.fps = fps self.writer: cv.VideoWriter | None = None @@ -13,23 +16,36 @@ def __init__(self, output_dir: str, img_size: tuple[int, int], fps: float = 30.0 self.current_path: str | None = None self.img_size = img_size os.makedirs(output_dir, exist_ok=True) + self.frames_recorded = 0 + self.frames_total = 0 + self.max_frames = int(fps * 60 * 5) # max 5 minutes per recording to prevent huge files - def extend_or_start(self, duration: float = 2.0): + def extend_or_start(self, duration: int = 2): """Start a new recording or extend current one by duration seconds.""" - self.stop_time = time.time() + duration - if self.writer is None: + _, _, free = shutil.disk_usage(self.output_dir) + min_space_bytes = 1 * 1024**3 # 1GB + if free < min_space_bytes: + logger.warning("Insufficient disk space: less than 1GB available. Not allowing new recording.") + return + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") self.current_path = f"{self.output_dir}/motion_{timestamp}.mp4" + logger.info(f"Motion detected. Starting new recording at: {self.current_path}") fourcc = cv.VideoWriter_fourcc(*'mp4v') self.writer = cv.VideoWriter(self.current_path, fourcc, self.fps, self.img_size) - - def write_frame(self, frame: np.ndarray): - """Write frame if recording is active. Stops if past stop_time.""" + self.frames_recorded = 0 + self.frames_total = 0 + + self.frames_total = self.frames_recorded + (duration * self.fps) + + def process_frame(self, frame: np.ndarray): + """Write frame if recording is active. Stops if past total frames or max frames.""" if self.writer is None: return - if time.time() < self.stop_time: + if self.frames_recorded < self.frames_total and self.frames_recorded < self.max_frames: + self.frames_recorded += 1 self.writer.write(frame) else: self.stop() @@ -37,5 +53,8 @@ def write_frame(self, frame: np.ndarray): def stop(self): if self.writer: self.writer.release() + logger.info(f"Saved recording: {self.current_path}") self.writer = None self.current_path = None + self.frames_recorded = 0 + self.frames_total = 0 diff --git a/waspinator/frame_provider.py b/waspinator/frame_provider.py index 8c10860..0fdc71c 100644 --- a/waspinator/frame_provider.py +++ b/waspinator/frame_provider.py @@ -28,7 +28,7 @@ def __init__(self, img_size): self.camera.set_controls({ "AfMode": 0, - "LensPosition": 5.4 + "LensPosition": 18 }) self.camera.start() diff --git a/waspinator/main.py b/waspinator/main.py index 19a586a..8bbb760 100644 --- a/waspinator/main.py +++ b/waspinator/main.py @@ -1,24 +1,24 @@ import argparse -from collections import deque import logging -import threading -from ultralytics.models import YOLO +import time from waspinator.decider import decide from waspinator.display import FrameDisplay from waspinator.event_recorder import EventRecorder from waspinator.frame_provider import get_frame_provider from waspinator.motion_detector import MotionDetector -from waspinator.trap import TrapController, FakeTrap, HardwareTrap, TrapState +from waspinator.trap import TrapCommand, TrapController, FakeTrap, HardwareTrap, TrapState +from waspinator.patience_countdown import PatienceCountdown import cv2 as cv +from multiprocessing import Queue, Process, Event +from dotenv import load_dotenv img_size = (640, 384) -history_length = 3 +sleep_duration = 0.05 logger = logging.getLogger(__name__) -def main(model=None, argv=None): - if model is None: - model = YOLO('./models/yolo26s-waspinator-chamber.pt', task='detect') +def main(argv=None): + model_path = './models/yolo26n-waspinator-chamber_ncnn_model' parser = argparse.ArgumentParser(description='Catch some vespa velutinas.') subparsers = parser.add_subparsers(dest='command', required=True) @@ -31,79 +31,166 @@ def main(model=None, argv=None): start_parser.add_argument("--log-level", default="INFO", choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]) start_parser.add_argument('--record', action='store_true', help='Record motion events to video files') + setup_parser = subparsers.add_parser('setup', help='Setup the waspinator trap') + setup_parser.add_argument("--pos", default="OPEN", help='', choices=["OPEN", "CLOSED"]) + setup_parser.add_argument("--log-level", default="INFO", choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]) + args = parser.parse_args(argv) logging.basicConfig( level=args.log_level, format='%(asctime)s.%(msecs)03d %(levelname)s %(name)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) + + servo_param=load_config(logger=logger) + if args.command == "start": - trap = FakeTrap() if args.dry_run else HardwareTrap() + trap = FakeTrap() if args.dry_run else HardwareTrap(servo_param['servo_open'], servo_param['servo_closed']) + trap.setup("OPEN") trap_controller = TrapController(trap) display = FrameDisplay(pause=args.step) if args.show else None motion_detector = MotionDetector() event_recorder = EventRecorder("./recordings", img_size) if args.record else None - with get_frame_provider(args.source, (4608, 2592)) as frame_provider: - trap_thread = TrapThread(frame_provider, model, trap, trap_controller) - trap_thread.start() + frame_queue = Queue(maxsize=1) + with get_frame_provider(args.source, (2304, 1296)) as frame_provider: + run_inference_event = Event() + shutdown_event = Event() + trap_process = Process( + target=trap_worker, + args=(frame_queue, model_path, trap, trap_controller, run_inference_event, shutdown_event) + ) + trap_process.start() while frame_provider.update(): frame = frame_provider.frame assert frame is not None frame = cv.resize(frame, img_size) - if motion_detector.has_motion(frame) and event_recorder is not None: - event_recorder.extend_or_start() + if motion_detector.has_motion(frame): + if not run_inference_event.is_set(): + logger.info("Motion detected; signaling trap process to run inference.") + run_inference_event.set() + if event_recorder is not None: + event_recorder.extend_or_start() + + + # Add to trap/inference queue + try: + # we use a queue of size 1 to always have the latest frame for inference, dropping older frames if the trap worker is still processing + if frame_queue.full(): + frame_queue.get_nowait() + except: + pass + frame_queue.put(frame) if event_recorder is not None: - event_recorder.write_frame(frame) + event_recorder.process_frame(frame) - if display and trap_thread.annotated_frame is not None: - if display.show_and_check_quit(trap_thread.annotated_frame): + if display: + if display.show_and_check_quit(frame): break + + time.sleep(sleep_duration) - trap_thread.stop() if display: display.close() if event_recorder: event_recorder.stop() + + shutdown_event.set() # signal trap_worker to exit + run_inference_event.set() # resume trap_worker so it can exit + try: + if frame_queue.full(): + frame_queue.get_nowait() + except: + pass + frame_queue.put(None) + trap_process.join() # wait for the trap_worker process to finish + + elif args.command == "setup": + trap = HardwareTrap(servo_param['servo_open'], servo_param['servo_closed']) + trap.setup(args.pos) else: parser.print_help() - -class TrapThread(threading.Thread): - def __init__(self, frame_provider, model, trap, trap_controller): - super().__init__(daemon=True) - self.running = False - self.frame_provider = frame_provider - self.model = model - self.trap = trap - self.trap_controller = trap_controller - self.annotated_frame = None - - def run(self): - summary_history = deque([], maxlen=history_length) - current_state = TrapState.READY_TO_TRIGGER - - self.running = True - while self.running: - frame = self.frame_provider.frame - if frame is None: - continue - result = self.model(frame, imgsz=img_size[0])[0] - self.annotated_frame = result.plot() - - summary_history.append(result.summary()) - - command, next_state = decide(current_state, summary_history, self.trap.ready()) - - self.trap_controller.handle_command(command) - - current_state = next_state - - def stop(self): - self.running = False +def trap_worker(frame_queue, model_path, trap, trap_controller: TrapController, run_inference_event, shutdown_event): + from collections import deque + from ultralytics.models import YOLO + history_length = 3 + patience_length = 30 + cooldown_seconds = 60 + wait_seconds = 60 + confidence_threshold = 0.8 + + model = YOLO(model_path, task='detect') + summary_history = deque([], maxlen=history_length) + current_state = TrapState.READY_TO_TRIGGER + patience_countdown = PatienceCountdown(patience_length) # After how many cycles of no detection do we pause the inference loop + + while True: + run_inference_event.wait() # Wait until the main process signals to run inference + if shutdown_event.is_set(): + logger.info("Shutdown event received; exiting trap worker.") + return + + frame = frame_queue.get() + if frame is None: + return + + result = model(frame, imgsz=img_size[0], conf=confidence_threshold)[0] + summary_history.append(result.summary()) + + command, next_state = decide(current_state, summary_history, trap.ready(), patience_countdown) + trap_controller.handle_command(command) + current_state = next_state + + if command != TrapCommand.NO_OP: + # we've reached a decision other than NO_OP. We need to clear the summary history to avoid making decisions based on old data. + summary_history.clear() + + if command == TrapCommand.SLEEP: + logger.info(f"No velutina detected for {patience_length} cycles. Entering cooldown for {cooldown_seconds} seconds.") + start = time.time() + while time.time() - start < cooldown_seconds: + if shutdown_event.is_set(): + logger.info("Shutdown event received during cooldown; exiting trap worker.") + return + time.sleep(1) + logger.info("Trap worker cooldown elapsed. Inference will resume on next motion detection.") + run_inference_event.clear() + elif command in [TrapCommand.WAIT, TrapCommand.TRIGGER]: + logger.info(f"Waiting for clearance. Next inference will run in {wait_seconds} seconds.") + start = time.time() + while time.time() - start < wait_seconds: + if shutdown_event.is_set(): + logger.info("Shutdown event received during wait; exiting trap worker.") + trap.reset() # make sure we reset the trap if we're exiting while waiting for clearance + return + time.sleep(1) + +def load_config(logger): + import os + + package_dir = os.path.dirname(os.path.abspath(__file__)) + + config_path = os.path.join(package_dir, ".config") + if not os.path.exists(config_path): + config_path = os.path.join(package_dir, ".config_default") + + load_dotenv(dotenv_path=config_path) + logger.info(f"Using config file: {config_path}") + + servo_open = float(os.getenv("SERVO_OPEN", 8.2)) + servo_closed = float(os.getenv("SERVO_CLOSED", 4.6)) + + servo_param = { + "servo_open": servo_open, + "servo_closed": servo_closed, + } + + logger.info("Loaded servo parameters: %s", servo_param) + return servo_param if __name__ == '__main__': main() diff --git a/waspinator/patience_countdown.py b/waspinator/patience_countdown.py new file mode 100644 index 0000000..51d63a3 --- /dev/null +++ b/waspinator/patience_countdown.py @@ -0,0 +1,15 @@ +class PatienceCountdown: + """A simple countdown timer that can be reset and ticked down. Used to implement a patience mechanism for the trap.""" + def __init__(self, start): + self.start = start + self.count = start + + def tick(self): + if self.count > 0: + self.count -= 1 + + def reset(self): + self.count = self.start + + def ran_out(self): + return self.count == 0 diff --git a/waspinator/trap.py b/waspinator/trap.py index 5cac709..6f43324 100644 --- a/waspinator/trap.py +++ b/waspinator/trap.py @@ -1,5 +1,7 @@ +from datetime import datetime from enum import Enum, auto import logging +from pathlib import Path import time logger = logging.getLogger(__name__) @@ -14,6 +16,8 @@ class TrapCommand(Enum): NO_OP = auto() TRIGGER = auto() RESET = auto() + SLEEP = auto() + WAIT = auto() class TrapController: def __init__(self, trap, initial_state=TrapState.WAITING_FOR_CLEARANCE): @@ -21,7 +25,6 @@ def __init__(self, trap, initial_state=TrapState.WAITING_FOR_CLEARANCE): self.state = initial_state def handle_command(self, command: TrapCommand): - logger.debug("Handling command: %s", command) if command == TrapCommand.TRIGGER: self.trap.trigger() elif command == TrapCommand.RESET: @@ -29,9 +32,6 @@ def handle_command(self, command: TrapCommand): elif command == TrapCommand.NO_OP: logger.debug("NO_OP command received; doing nothing.") - # TODO handle other commands - - class FakeTrap: """A fake trap implementation for dry-run mode, when we don't want to trigger actual hardware.""" def trigger(self): @@ -43,11 +43,16 @@ def ready(self) -> bool: def reset(self): logger.info("FAKE TRAP RESET") + def setup(self, pos:str): + pass + class HardwareTrap: - def __init__(self): + def __init__(self, servo_open:float,servo_closed:float): from rpi_hardware_pwm import HardwarePWM - self.servo = HardwarePWM(pwm_channel=2, chip=0, hz=60) + self.servo = HardwarePWM(pwm_channel=2, chip=0, hz=50) + self.servo_open = servo_open + self.servo_closed = servo_closed self.last_movement = time.time() """A trap abstraction that should trigger actual hardware.""" @@ -56,11 +61,17 @@ def trigger(self): if not self.ready(): logger.warning("Trap not ready! Trigger aborted.") return - self.servo.start(12.5) - time.sleep(1) + self.servo.start(self.servo_closed) + time.sleep(2) self.servo.stop() + self._create_trigger_file() self.last_movement = time.time() + def _create_trigger_file(self): + Path("./recordings").mkdir(exist_ok=True) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + Path(f"./recordings/motion_{timestamp}_trigger.txt").touch() + def ready(self): return (time.time() - self.last_movement > COOLDOWN_SECONDS) @@ -69,7 +80,21 @@ def reset(self): if not self.ready(): logger.warning("Trap not ready! Reset aborted.") return - self.servo.start(8) - time.sleep(1) + self.servo.start(self.servo_open) + time.sleep(2) self.servo.stop() self.last_movement = time.time() + + + def setup(self, pos:str): + logger.info("Setting up hardware trap...") + if pos == "OPEN": + self.servo.start(self.servo_open) + time.sleep(2) + self.servo.stop() + self.last_movement = time.time() + else: + self.servo.start(self.servo_closed) + time.sleep(2) + self.servo.stop() + self.last_movement = time.time()