Thomas Buck 2 роки тому
коміт
d38af76110

+ 1
- 0
.gitignore Переглянути файл

@@ -0,0 +1 @@
1
+firmware/.pio

+ 19
- 0
README.md Переглянути файл

@@ -0,0 +1,19 @@
1
+# OpenChrono
2
+
3
+Chronograph for Airsoft use, released as Free Open Source hardware and software!
4
+Uses a 3D printed housing to hold an Arduino, an OLED, batteries and two photosensitive light barriers.
5
+
6
+## Hardware
7
+
8
+Take the STL files from the 'hardware' directory or modify the included OpenSCAD design and create your own custom STLs.
9
+
10
+Required Materials:
11
+
12
+TODO
13
+
14
+## Software
15
+
16
+This project uses the [U8g2 library by olikraus](https://github.com/olikraus/u8g2) to draw to the I2C OLED display.
17
+
18
+You can compile and flash the software using either PlatformIO or the standard Arduino IDE.
19
+In the latter case, install the U8g2 library using the Arduino IDE Library Manager and then flash as usual.

+ 1
- 0
firmware/.gitignore Переглянути файл

@@ -0,0 +1 @@
1
+.pio

+ 62
- 0
firmware/OpenChrono/OpenChrono.ino Переглянути файл

@@ -0,0 +1,62 @@
1
+/*
2
+ * OpenChrono.ino
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ *
8
+ * The goal is to measure the speed of a BB moving through the device.
9
+ */
10
+
11
+#include "timing.h"
12
+#include "ticks.h"
13
+#include "lcd.h"
14
+#include "config.h"
15
+
16
+static void calculate(uint16_t a, uint16_t b) {
17
+    uint16_t ticks = 0;
18
+
19
+    if (b >= a) {
20
+        // simple case - just return difference
21
+        ticks = b - a;
22
+    } else {
23
+        // the timer overflowed between measurements!
24
+        uint32_t tmp = ((uint32_t)b) - ((uint32_t)a);
25
+        tmp += 0x10000;
26
+        ticks = (uint16_t)tmp;
27
+    }
28
+
29
+    tick_new_value(ticks);
30
+    lcd_new_value();
31
+}
32
+
33
+static void measure() {
34
+    cli(); // disable interrupts before interacting with values
35
+
36
+    // reset interrupts
37
+    trigger_a = 0;
38
+    trigger_b = 0;
39
+
40
+    uint16_t a = time_a, b = time_b;
41
+
42
+    sei(); // enable interrupts immediately afterwards
43
+
44
+    calculate(a, b);
45
+}
46
+
47
+void setup() {
48
+    lcd_init();
49
+    delay(SCREEN_TIMEOUT); // show splash screen
50
+
51
+    timer_init();
52
+    interrupt_init();
53
+}
54
+
55
+void loop() {
56
+    if ((time_a == 1) && (time_b == 1)) {
57
+        // we got an event on both inputs
58
+        measure();
59
+    }
60
+
61
+    lcd_loop();
62
+}

+ 37
- 0
firmware/OpenChrono/adc.cpp Переглянути файл

@@ -0,0 +1,37 @@
1
+/*
2
+ * adc.cpp
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * https://provideyourown.com/2012/secret-arduino-voltmeter-measure-battery-voltage/
7
+ */
8
+
9
+#include <Arduino.h>
10
+
11
+#include "adc.h"
12
+
13
+long readVcc(void) {
14
+    // Read 1.1V reference against AVcc
15
+    // set the reference to Vcc and the measurement to the internal 1.1V reference
16
+#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
17
+    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
18
+#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
19
+    ADMUX = _BV(MUX5) | _BV(MUX0);
20
+#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
21
+    ADMUX = _BV(MUX3) | _BV(MUX2);
22
+#else
23
+    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
24
+#endif
25
+
26
+    delay(2); // Wait for Vref to settle
27
+    ADCSRA |= _BV(ADSC); // Start conversion
28
+    while (bit_is_set(ADCSRA,ADSC)); // measuring
29
+
30
+    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
31
+    uint8_t high = ADCH; // unlocks both
32
+
33
+    long result = (high<<8) | low;
34
+
35
+    result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000
36
+    return result; // Vcc in millivolts
37
+}

+ 15
- 0
firmware/OpenChrono/adc.h Переглянути файл

@@ -0,0 +1,15 @@
1
+/*
2
+ * adc.cpp
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * https://provideyourown.com/2012/secret-arduino-voltmeter-measure-battery-voltage/
7
+ */
8
+
9
+#ifndef __ADC_H__
10
+#define __ADC_H__
11
+
12
+// Vcc in millivolts
13
+long readVcc(void);
14
+
15
+#endif // __ADC_H__

+ 83
- 0
firmware/OpenChrono/config.h Переглянути файл

@@ -0,0 +1,83 @@
1
+/*
2
+ * config.h
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ */
8
+
9
+#ifndef __CONFIG_H__
10
+#define __CONFIG_H__
11
+
12
+// --------------------------------------
13
+
14
+#define VERSION "0.0.1"
15
+
16
+// --------------------------------------
17
+
18
+// hardware details
19
+
20
+#define SENSOR_DISTANCE 70.0 /* in mm */
21
+#define BB_WEIGHT 0.25 /* in g */
22
+
23
+// --------------------------------------
24
+
25
+// which unit to show on history screen
26
+
27
+#define METRIC 0
28
+#define IMPERIAL 1
29
+#define JOULES 2
30
+
31
+#define PREFERRED_UNITS JOULES
32
+
33
+// --------------------------------------
34
+
35
+// order and duration of screens
36
+
37
+#define SCREEN_CURRENT 0
38
+#define SCREEN_MIN 1
39
+#define SCREEN_AVERAGE 2
40
+#define SCREEN_MAX 3
41
+#define SCREEN_HISTORY 4
42
+
43
+#define SCREEN_ROTATION { \
44
+    SCREEN_CURRENT,       \
45
+    SCREEN_CURRENT,       \
46
+    SCREEN_MIN,           \
47
+    SCREEN_AVERAGE,       \
48
+    SCREEN_MAX,           \
49
+    SCREEN_HISTORY,       \
50
+    SCREEN_HISTORY        \
51
+}
52
+
53
+#define SCREEN_TIMEOUT 2500 /* in ms */
54
+
55
+// --------------------------------------
56
+
57
+// lcd config
58
+
59
+#define LCD_TYPE U8G2_SSD1306_128X64_NONAME_F_HW_I2C
60
+
61
+#define HEADING_FONT u8g2_font_VCR_OSD_tr
62
+#define TEXT_FONT u8g2_font_NokiaLargeBold_tr
63
+
64
+// --------------------------------------
65
+
66
+// history for graph of speeds.
67
+// should not be too big!
68
+
69
+#define HISTORY_BUFFER 50
70
+
71
+// --------------------------------------
72
+
73
+// placeholder data for debugging purposes
74
+
75
+#define DEBUG_TICK_COUNT 0
76
+#define DEBUG_TICK_DATA {}
77
+
78
+//#define DEBUG_TICK_COUNT 8
79
+//#define DEBUG_TICK_DATA { 8000, 10000, 9000, 11000, 8500, 9500, 10000, 7000 }
80
+
81
+// --------------------------------------
82
+
83
+#endif // __CONFIG_H__

+ 283
- 0
firmware/OpenChrono/lcd.cpp Переглянути файл

@@ -0,0 +1,283 @@
1
+/*
2
+ * lcd.cpp
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ *
8
+ * SSD1306 OLED display connected via I2C.
9
+ */
10
+
11
+#include <Arduino.h>
12
+
13
+#include "ticks.h"
14
+#include "adc.h"
15
+#include "lcd.h"
16
+#include "config.h"
17
+
18
+#include <Wire.h>
19
+#include <U8g2lib.h>
20
+
21
+static uint8_t screens[] = SCREEN_ROTATION;
22
+static uint8_t lcd_screen = 0;
23
+static uint64_t lcd_rotate_time = 0;
24
+
25
+static LCD_TYPE u8g2(U8G2_R0, U8X8_PIN_NONE);
26
+
27
+void lcd_init(void) {
28
+    u8g2.begin();
29
+    u8g2.setFontPosBottom();
30
+
31
+    u8g2.clearBuffer();
32
+    u8g2.setFlipMode(1);
33
+
34
+    String s = F("OpenChrono");
35
+    u8g2.setFont(HEADING_FONT);
36
+    uint8_t heading_height = u8g2.getMaxCharHeight();
37
+    u8g2.drawStr(
38
+        0,
39
+        heading_height,
40
+        s.c_str()
41
+    );
42
+
43
+    s = F("Version ");
44
+    s += F(VERSION);
45
+    u8g2.setFont(TEXT_FONT);
46
+    u8g2.drawStr(
47
+        (u8g2.getDisplayWidth() - u8g2.getStrWidth(s.c_str())) / 2,
48
+        heading_height + u8g2.getMaxCharHeight() + 4,
49
+        s.c_str()
50
+    );
51
+
52
+    s = String((double)SENSOR_DISTANCE, 0);
53
+    s += F("mm");
54
+    u8g2.setFont(TEXT_FONT);
55
+    u8g2.drawStr(
56
+        0,
57
+        u8g2.getDisplayHeight() - u8g2.getMaxCharHeight() - 4,
58
+        s.c_str()
59
+    );
60
+
61
+    s = String((double)readVcc() / 1000.0, 1);
62
+    s += F("V");
63
+    u8g2.setFont(TEXT_FONT);
64
+    u8g2.drawStr(
65
+        (u8g2.getDisplayWidth() - u8g2.getStrWidth(s.c_str())) / 2,
66
+        u8g2.getDisplayHeight() - u8g2.getMaxCharHeight() - 4,
67
+        s.c_str()
68
+    );
69
+
70
+    s = String((double)BB_WEIGHT, 2);
71
+    s += F("g");
72
+    u8g2.setFont(TEXT_FONT);
73
+    u8g2.drawStr(
74
+        u8g2.getDisplayWidth() - u8g2.getStrWidth(s.c_str()),
75
+        u8g2.getDisplayHeight() - u8g2.getMaxCharHeight() - 4,
76
+        s.c_str()
77
+    );
78
+
79
+    s = F("by xythobuz.de");
80
+    u8g2.setFont(TEXT_FONT);
81
+    u8g2.drawStr(
82
+        (u8g2.getDisplayWidth() - u8g2.getStrWidth(s.c_str())) / 2,
83
+        u8g2.getDisplayHeight() - 1,
84
+        s.c_str()
85
+    );
86
+
87
+    u8g2.sendBuffer();
88
+}
89
+
90
+void lcd_new_value(void) {
91
+    lcd_rotate_time = millis();
92
+    lcd_screen = 0;
93
+    lcd_draw(screens[lcd_screen]);
94
+}
95
+
96
+void lcd_draw(uint8_t screen) {
97
+    // fall back to first screen when no more data available
98
+    if (tick_count <= 1) {
99
+        screen = SCREEN_CURRENT;
100
+    }
101
+
102
+    if ((screen == SCREEN_CURRENT) || (screen == SCREEN_AVERAGE)
103
+            || (screen == SCREEN_MIN) || (screen == SCREEN_MAX)) {
104
+        if (tick_count < 1) {
105
+            u8g2.clearBuffer();
106
+            u8g2.setFlipMode(1);
107
+
108
+            String s = F("Ready!");
109
+            u8g2.setFont(HEADING_FONT);
110
+            u8g2.drawStr(
111
+                (u8g2.getDisplayWidth() - u8g2.getStrWidth(s.c_str())) / 2,
112
+                (u8g2.getDisplayHeight() + u8g2.getMaxCharHeight()) / 2,
113
+                s.c_str()
114
+            );
115
+
116
+            u8g2.sendBuffer();
117
+            return;
118
+        }
119
+
120
+        uint16_t tick = 0;
121
+
122
+        if (screen == SCREEN_CURRENT) {
123
+            // only show most recent value
124
+            tick = tick_history[tick_count - 1];
125
+        } else if (screen == SCREEN_AVERAGE) {
126
+            tick = tick_average();
127
+        } else if (screen == SCREEN_MAX) {
128
+            tick = tick_min();
129
+        } else if (screen == SCREEN_MIN) {
130
+            tick = tick_max();
131
+        }
132
+
133
+        double metric = tick_to_metric(tick);
134
+        double imperial = metric_to_imperial(metric);
135
+        double joules = metric_to_joules(metric, BB_WEIGHT);
136
+
137
+        u8g2.clearBuffer();
138
+        u8g2.setFlipMode(1);
139
+        u8g2.setFont(TEXT_FONT);
140
+
141
+        String s;
142
+
143
+        if (screen == SCREEN_CURRENT) {
144
+            s = F("Last Shot (No. ");
145
+        } else if (screen == SCREEN_AVERAGE) {
146
+            s = F("Average (of ");
147
+        } else if (screen == SCREEN_MAX) {
148
+            s = F("Maximum (of ");
149
+        } else if (screen == SCREEN_MIN) {
150
+            s = F("Minimum (of ");
151
+        }
152
+        s += String(tick_count);
153
+        s += F(")");
154
+
155
+        u8g2.drawStr(
156
+            (u8g2.getDisplayWidth() - u8g2.getStrWidth(s.c_str())) / 2,
157
+            u8g2.getMaxCharHeight(),
158
+            s.c_str()
159
+        );
160
+
161
+        s = String(metric, 0);
162
+        s += F(" m/s");
163
+        u8g2.drawStr(
164
+            0,
165
+            u8g2.getMaxCharHeight() * 2 + 1,
166
+            s.c_str()
167
+        );
168
+
169
+        s = String(imperial, 0);
170
+        s += F(" FPS");
171
+        u8g2.drawStr(
172
+            0,
173
+            u8g2.getMaxCharHeight() * 3 + 2,
174
+            s.c_str()
175
+        );
176
+
177
+        s = String(joules, 2);
178
+        s += F(" J");
179
+        u8g2.drawStr(
180
+            0,
181
+            u8g2.getMaxCharHeight() * 4 + 3,
182
+            s.c_str()
183
+        );
184
+
185
+        u8g2.sendBuffer();
186
+    } else if (screen == SCREEN_HISTORY) {
187
+        uint16_t min = tick_max();
188
+        uint16_t max = tick_min();
189
+        String s;
190
+
191
+        u8g2.clearBuffer();
192
+        u8g2.setFlipMode(1);
193
+        u8g2.setFont(TEXT_FONT);
194
+
195
+        // max text
196
+        double max_metric = tick_to_metric(max);
197
+        if (PREFERRED_UNITS == METRIC) {
198
+            s = String(max_metric, 0);
199
+        } else if (PREFERRED_UNITS == IMPERIAL) {
200
+            s = String(metric_to_imperial(max_metric), 0);
201
+        } else {
202
+            s = String(metric_to_joules(max_metric, BB_WEIGHT), 2);
203
+        }
204
+        uint8_t l1 = u8g2.getStrWidth(s.c_str());
205
+        u8g2.drawStr(
206
+            0,
207
+            u8g2.getMaxCharHeight(),
208
+            s.c_str()
209
+        );
210
+
211
+        // unit indicator
212
+        uint8_t l2;
213
+        if (PREFERRED_UNITS == METRIC) {
214
+            s = F("m/s");
215
+            l2 = u8g2.getStrWidth(s.c_str());
216
+            u8g2.drawStr(
217
+                0,
218
+                (u8g2.getDisplayHeight() + u8g2.getMaxCharHeight()) / 2,
219
+                s.c_str()
220
+            );
221
+        } else if (PREFERRED_UNITS == IMPERIAL) {
222
+            s = F("FPS");
223
+            l2 = u8g2.getStrWidth(s.c_str());
224
+            u8g2.drawStr(
225
+                0,
226
+                (u8g2.getDisplayHeight() + u8g2.getMaxCharHeight()) / 2,
227
+                s.c_str()
228
+            );
229
+        } else {
230
+            s = F("J");
231
+            l2 = u8g2.getStrWidth(s.c_str());
232
+            u8g2.drawStr(
233
+                0,
234
+                (u8g2.getDisplayHeight() + u8g2.getMaxCharHeight()) / 2,
235
+                s.c_str()
236
+            );
237
+        }
238
+
239
+        // min text
240
+        double min_metric = tick_to_metric(min);
241
+        if (PREFERRED_UNITS == METRIC) {
242
+            s = String(min_metric, 0);
243
+        } else if (PREFERRED_UNITS == IMPERIAL) {
244
+            s = String(metric_to_imperial(min_metric), 0);
245
+        } else {
246
+            s = String(metric_to_joules(min_metric, BB_WEIGHT), 2);
247
+        }
248
+        uint8_t l3 = u8g2.getStrWidth(s.c_str());
249
+        u8g2.drawStr(
250
+            0,
251
+            u8g2.getDisplayHeight() - 1,
252
+            s.c_str()
253
+        );
254
+
255
+        uint8_t lmax = max(max(l1, l2), l3);
256
+        uint8_t graph_start = lmax + 1;
257
+
258
+        // graph lines
259
+        uint8_t segment_w = (u8g2.getDisplayWidth() - graph_start) / (tick_count - 1);
260
+        for (int i = 0; i < tick_count - 1; i++) {
261
+            u8g2.drawLine(
262
+                graph_start + (i * segment_w),
263
+                map(tick_history[i], min, max, 0, u8g2.getDisplayHeight() - 1),
264
+                graph_start + ((i + 1) * segment_w),
265
+                map(tick_history[i + 1], min, max, 0, u8g2.getDisplayHeight() - 1)
266
+            );
267
+        }
268
+
269
+        u8g2.sendBuffer();
270
+    }
271
+}
272
+
273
+void lcd_loop(void) {
274
+    if ((millis() - lcd_rotate_time) > SCREEN_TIMEOUT) {
275
+        lcd_rotate_time = millis();
276
+        lcd_screen++;
277
+        if (lcd_screen >= sizeof(screens)) {
278
+            lcd_screen = 0;
279
+        }
280
+
281
+        lcd_draw(screens[lcd_screen]);
282
+    }
283
+}

+ 19
- 0
firmware/OpenChrono/lcd.h Переглянути файл

@@ -0,0 +1,19 @@
1
+/*
2
+ * lcd.h
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ *
8
+ * SSD1306 OLED display connected via I2C.
9
+ */
10
+
11
+#ifndef __LCD_H__
12
+#define __LCD_H__
13
+
14
+void lcd_init(void);
15
+void lcd_new_value(void);
16
+void lcd_draw(uint8_t screen);
17
+void lcd_loop(void);
18
+
19
+#endif // __LCD_H__

+ 119
- 0
firmware/OpenChrono/ticks.cpp Переглянути файл

@@ -0,0 +1,119 @@
1
+/*
2
+ * ticks.cpp
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ *
8
+ * Some notes about time calculations:
9
+ * We have Timer1 running with 16MHz
10
+ * which gives us a tick period of 62.5ns.
11
+ *
12
+ * The distance of the sensors is given in mm
13
+ * in SENSOR_DISTANCE (eg. 70mm).
14
+ *
15
+ * period_in_s = 1 / F_CPU
16
+ * period_in_s = 62.5 / 1000 / 1000 / 1000;
17
+ * dist_in_m = SENSOR_DISTANCE * 0.1 * 0.01
18
+ * travel_time_in_s = ticks * period_in_s;
19
+ * speed = dist_in_m / travel_time_in_s;
20
+ *
21
+ * period_in_s = 0.0000000625
22
+ * dist_in_m = 0.07m
23
+ * ticks = 2000
24
+ * travel_time_in_s = 0.000125
25
+ * speed = 560 m/s
26
+ *
27
+ * speed = (SENSOR_DISTANCE / 1000) / (ticks * 62.5 / 1000 / 1000 / 1000)
28
+ * speed = SENSOR_DISTANCE / (ticks * 62.5 / 1000 / 1000)
29
+ * speed = SENSOR_DISTANCE / (ticks * 1000 / F_CPU)
30
+ *
31
+ * Because we can at max measure 0xFFFF ticks
32
+ * this gives us a slowest speed we can measure.
33
+ * 0xFFFF = 65535 ticks
34
+ * speed = SENSOR_DISTANCE / (65535 * 1000 / F_CPU)
35
+ * so we can measure from 17m/s (61km/h, approx. 0.03J @ 0.2g)
36
+ * up to ridulous 1120000m/s (4032000km/h)
37
+ */
38
+
39
+#include <Arduino.h>
40
+
41
+#include "ticks.h"
42
+#include "config.h"
43
+
44
+uint16_t tick_history[HISTORY_BUFFER] = DEBUG_TICK_DATA;
45
+uint8_t tick_count = DEBUG_TICK_COUNT;
46
+
47
+void tick_new_value(uint16_t ticks) {
48
+    // store new value in history buffer
49
+    if (tick_count < HISTORY_BUFFER) {
50
+        tick_history[tick_count] = ticks;
51
+        tick_count++;
52
+    } else {
53
+        for (uint8_t i = 0; i < HISTORY_BUFFER - 1; i++) {
54
+            tick_history[i] = tick_history[i + 1];
55
+        }
56
+        tick_history[HISTORY_BUFFER - 1] = ticks;
57
+    }
58
+}
59
+
60
+uint16_t tick_average() {
61
+    if (tick_count == 0) {
62
+        return 0;
63
+    }
64
+
65
+    uint64_t sum = 0;
66
+    for (uint8_t i = 0; i < tick_count; i++) {
67
+        sum += tick_history[i];
68
+    }
69
+    sum /= (uint64_t)tick_count;
70
+    return (uint16_t)sum;
71
+}
72
+
73
+uint16_t tick_max() {
74
+    if (tick_count == 0) {
75
+        return 0;
76
+    }
77
+
78
+    uint16_t cmp = 0;
79
+    for (uint8_t i = 0; i < tick_count; i++) {
80
+        if (tick_history[i] > cmp) {
81
+            cmp = tick_history[i];
82
+        }
83
+    }
84
+    return cmp;
85
+}
86
+
87
+uint16_t tick_min() {
88
+    if (tick_count == 0) {
89
+        return 0;
90
+    }
91
+
92
+    uint16_t cmp = 0xFFFF;
93
+    for (uint8_t i = 0; i < tick_count; i++) {
94
+        if (tick_history[i] < cmp) {
95
+            cmp = tick_history[i];
96
+        }
97
+    }
98
+    return cmp;
99
+}
100
+
101
+double tick_to_metric(uint16_t ticks) {
102
+    // v = d / t
103
+    double period = 1000.0 / ((double)(F_CPU));
104
+    double time = period * (double)ticks;
105
+    double speed = (double)SENSOR_DISTANCE / time;
106
+    return speed;
107
+}
108
+
109
+double metric_to_imperial(double speed) {
110
+    // convert m/s to f/s
111
+    speed *= 3.28084;
112
+    return speed;
113
+}
114
+
115
+double metric_to_joules(double speed, double mass) {
116
+    // e = 0.5 * m * v^2
117
+    double energy = 0.5 * mass * speed * speed / 1000.0;
118
+    return energy;
119
+}

+ 23
- 0
firmware/OpenChrono/ticks.h Переглянути файл

@@ -0,0 +1,23 @@
1
+/*
2
+ * ticks.h
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ */
8
+
9
+#ifndef __TICKS_H__
10
+#define __TICKS_H__
11
+
12
+extern uint16_t tick_history[];
13
+extern uint8_t tick_count;
14
+
15
+void tick_new_value(uint16_t ticks);
16
+uint16_t tick_average();
17
+uint16_t tick_max();
18
+uint16_t tick_min();
19
+double tick_to_metric(uint16_t ticks);
20
+double metric_to_imperial(double speed);
21
+double metric_to_joules(double speed, double mass);
22
+
23
+#endif // __TICKS_H__

+ 48
- 0
firmware/OpenChrono/timing.cpp Переглянути файл

@@ -0,0 +1,48 @@
1
+/*
2
+ * timing.cpp
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ *
8
+ * Two phototransistors connected to external interrupts 0 and 1.
9
+ */
10
+
11
+#include <Arduino.h>
12
+
13
+#include "timing.h"
14
+#include "config.h"
15
+
16
+volatile uint8_t trigger_a = 0, trigger_b = 0;
17
+volatile uint16_t time_a = 0, time_b = 0;
18
+
19
+void interrupt_init() {
20
+    // trigger both on rising edge
21
+    EICRA = (1 << ISC00) | (1 << ISC01);
22
+    EICRA |= (1 << ISC10) | (1 << ISC11);
23
+
24
+    // enable interrupts
25
+    EIMSK = (1 << INT0) | (1 << INT1);
26
+}
27
+
28
+ISR(INT0_vect) {
29
+    time_a = timer_get();
30
+    trigger_a = 1;
31
+}
32
+
33
+ISR(INT1_vect) {
34
+    time_b = timer_get();
35
+    trigger_b = 1;
36
+}
37
+
38
+// --------------------------------------
39
+
40
+void timer_init() {
41
+    // normal mode, prescaler 1
42
+    TCCR1A = 0;
43
+    TCCR1B = (1 << CS10);
44
+}
45
+
46
+uint16_t timer_get() {
47
+    return TCNT1;
48
+}

+ 22
- 0
firmware/OpenChrono/timing.h Переглянути файл

@@ -0,0 +1,22 @@
1
+/*
2
+ * timing.h
3
+ *
4
+ * OpenChrono BB speed measurement device.
5
+ *
6
+ * Copyright (c) 2022 Thomas Buck <thomas@xythobuz.de>
7
+ *
8
+ * Two phototransistors connected to external interrupts 0 and 1.
9
+ */
10
+
11
+#ifndef __TIMING_H__
12
+#define __TIMING_H__
13
+
14
+extern volatile uint8_t trigger_a, trigger_b;
15
+extern volatile uint16_t time_a, time_b;
16
+
17
+void interrupt_init();
18
+
19
+void timer_init();
20
+uint16_t timer_get();
21
+
22
+#endif // __TIMING_H__

+ 12
- 0
firmware/platformio.ini Переглянути файл

@@ -0,0 +1,12 @@
1
+[platformio]
2
+default_envs = arduino
3
+src_dir = OpenChrono
4
+
5
+[env:arduino]
6
+platform = atmelavr
7
+framework = arduino
8
+board = nanoatmega328
9
+lib_deps =
10
+  SPI
11
+  Wire
12
+  olikraus/U8g2@^2.33.4

+ 382
- 0
hardware/extern/threads.scad Переглянути файл

@@ -0,0 +1,382 @@
1
+/*
2
+ * ISO-standard metric threads, following this specification:
3
+ *          http://en.wikipedia.org/wiki/ISO_metric_screw_thread
4
+ *
5
+ * Copyright 2020 Dan Kirshner - dan_kirshner@yahoo.com
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
+ * GNU General Public License for more details.
15
+ *
16
+ * See <http://www.gnu.org/licenses/>.
17
+ *
18
+ * Version 2.4.  2019-07-14  Add test option - do not render threads.
19
+ * Version 2.3.  2017-08-31  Default for leadin: 0 (best for internal threads).
20
+ * Version 2.2.  2017-01-01  Correction for angle; leadfac option.  (Thanks to
21
+ *                           Andrew Allen <a2intl@gmail.com>.)
22
+ * Version 2.1.  2016-12-04  Chamfer bottom end (low-z); leadin option.
23
+ * Version 2.0.  2016-11-05  Backwards compatibility (earlier OpenSCAD) fixes.
24
+ * Version 1.9.  2016-07-03  Option: tapered.
25
+ * Version 1.8.  2016-01-08  Option: (non-standard) angle.
26
+ * Version 1.7.  2015-11-28  Larger x-increment - for small-diameters.
27
+ * Version 1.6.  2015-09-01  Options: square threads, rectangular threads.
28
+ * Version 1.5.  2015-06-12  Options: thread_size, groove.
29
+ * Version 1.4.  2014-10-17  Use "faces" instead of "triangles" for polyhedron
30
+ * Version 1.3.  2013-12-01  Correct loop over turns -- don't have early cut-off
31
+ * Version 1.2.  2012-09-09  Use discrete polyhedra rather than linear_extrude ()
32
+ * Version 1.1.  2012-09-07  Corrected to right-hand threads!
33
+ */
34
+
35
+// Examples.
36
+//
37
+// Standard M8 x 1.
38
+// metric_thread (diameter=8, pitch=1, length=4);
39
+
40
+// Square thread.
41
+// metric_thread (diameter=8, pitch=1, length=4, square=true);
42
+
43
+// Non-standard: long pitch, same thread size.
44
+//metric_thread (diameter=8, pitch=4, length=4, thread_size=1, groove=true);
45
+
46
+// Non-standard: 20 mm diameter, long pitch, square "trough" width 3 mm,
47
+// depth 1 mm.
48
+//metric_thread (diameter=20, pitch=8, length=16, square=true, thread_size=6,
49
+//               groove=true, rectangle=0.333);
50
+
51
+// English: 1/4 x 20.
52
+//english_thread (diameter=1/4, threads_per_inch=20, length=1);
53
+
54
+// Tapered.  Example -- pipe size 3/4" -- per:
55
+// http://www.engineeringtoolbox.com/npt-national-pipe-taper-threads-d_750.html
56
+// english_thread (diameter=1.05, threads_per_inch=14, length=3/4, taper=1/16);
57
+
58
+// Thread for mounting on Rohloff hub.
59
+//difference () {
60
+//   cylinder (r=20, h=10, $fn=100);
61
+//
62
+//   metric_thread (diameter=34, pitch=1, length=10, internal=true, n_starts=6);
63
+//}
64
+
65
+
66
+// ----------------------------------------------------------------------------
67
+function segments (diameter) = min (50, max (ceil (diameter*6), 25));
68
+
69
+
70
+// ----------------------------------------------------------------------------
71
+// diameter -    outside diameter of threads in mm. Default: 8.
72
+// pitch    -    thread axial "travel" per turn in mm.  Default: 1.
73
+// length   -    overall axial length of thread in mm.  Default: 1.
74
+// internal -    true = clearances for internal thread (e.g., a nut).
75
+//               false = clearances for external thread (e.g., a bolt).
76
+//               (Internal threads should be "cut out" from a solid using
77
+//               difference ()).  Default: false.
78
+// n_starts -    Number of thread starts (e.g., DNA, a "double helix," has
79
+//               n_starts=2).  See wikipedia Screw_thread.  Default: 1.
80
+// thread_size - (non-standard) axial width of a single thread "V" - independent
81
+//               of pitch.  Default: same as pitch.
82
+// groove      - (non-standard) true = subtract inverted "V" from cylinder
83
+//                (rather thanadd protruding "V" to cylinder).  Default: false.
84
+// square      - true = square threads (per
85
+//               https://en.wikipedia.org/wiki/Square_thread_form).  Default:
86
+//               false.
87
+// rectangle   - (non-standard) "Rectangular" thread - ratio depth/(axial) width
88
+//               Default: 0 (standard "v" thread).
89
+// angle       - (non-standard) angle (deg) of thread side from perpendicular to
90
+//               axis (default = standard = 30 degrees).
91
+// taper       - diameter change per length (National Pipe Thread/ANSI B1.20.1
92
+//               is 1" diameter per 16" length). Taper decreases from 'diameter'
93
+//               as z increases.  Default: 0 (no taper).
94
+// leadin      - 0 (default): no chamfer; 1: chamfer (45 degree) at max-z end;
95
+//               2: chamfer at both ends, 3: chamfer at z=0 end.
96
+// leadfac     - scale of leadin chamfer (default: 1.0 = 1/2 thread).
97
+// test        - true = do not render threads (just draw "blank" cylinder).
98
+//               Default: false (draw threads).
99
+module metric_thread (diameter=8, pitch=1, length=1, internal=false, n_starts=1,
100
+                      thread_size=-1, groove=false, square=false, rectangle=0,
101
+                      angle=30, taper=0, leadin=0, leadfac=1.0, test=false)
102
+{
103
+   // thread_size: size of thread "V" different than travel per turn (pitch).
104
+   // Default: same as pitch.
105
+   local_thread_size = thread_size == -1 ? pitch : thread_size;
106
+   local_rectangle = rectangle ? rectangle : 1;
107
+
108
+   n_segments = segments (diameter);
109
+   h = (test && ! internal) ? 0 : (square || rectangle) ? local_thread_size*local_rectangle/2 : local_thread_size / (2 * tan(angle));
110
+
111
+   h_fac1 = (square || rectangle) ? 0.90 : 0.625;
112
+
113
+   // External thread includes additional relief.
114
+   h_fac2 = (square || rectangle) ? 0.95 : 5.3/8;
115
+
116
+   tapered_diameter = diameter - length*taper;
117
+
118
+   difference () {
119
+      union () {
120
+         if (! groove) {
121
+            if (! test) {
122
+               metric_thread_turns (diameter, pitch, length, internal, n_starts,
123
+                                    local_thread_size, groove, square, rectangle, angle,
124
+                                    taper);
125
+            }
126
+         }
127
+
128
+         difference () {
129
+
130
+            // Solid center, including Dmin truncation.
131
+            if (groove) {
132
+               cylinder (r1=diameter/2, r2=tapered_diameter/2,
133
+                         h=length, $fn=n_segments);
134
+            } else if (internal) {
135
+               cylinder (r1=diameter/2 - h*h_fac1, r2=tapered_diameter/2 - h*h_fac1,
136
+                         h=length, $fn=n_segments);
137
+            } else {
138
+
139
+               // External thread.
140
+               cylinder (r1=diameter/2 - h*h_fac2, r2=tapered_diameter/2 - h*h_fac2,
141
+                         h=length, $fn=n_segments);
142
+            }
143
+
144
+            if (groove) {
145
+               if (! test) {
146
+                  metric_thread_turns (diameter, pitch, length, internal, n_starts,
147
+                                       local_thread_size, groove, square, rectangle,
148
+                                       angle, taper);
149
+               }
150
+            }
151
+         }
152
+      }
153
+
154
+      // chamfer z=0 end if leadin is 2 or 3
155
+      if (leadin == 2 || leadin == 3) {
156
+         difference () {
157
+            cylinder (r=diameter/2 + 1, h=h*h_fac1*leadfac, $fn=n_segments);
158
+
159
+            cylinder (r2=diameter/2, r1=diameter/2 - h*h_fac1*leadfac, h=h*h_fac1*leadfac,
160
+                      $fn=n_segments);
161
+         }
162
+      }
163
+
164
+      // chamfer z-max end if leadin is 1 or 2.
165
+      if (leadin == 1 || leadin == 2) {
166
+         translate ([0, 0, length + 0.05 - h*h_fac1*leadfac]) {
167
+            difference () {
168
+               cylinder (r=diameter/2 + 1, h=h*h_fac1*leadfac, $fn=n_segments);
169
+               cylinder (r1=tapered_diameter/2, r2=tapered_diameter/2 - h*h_fac1*leadfac, h=h*h_fac1*leadfac,
170
+                         $fn=n_segments);
171
+            }
172
+         }
173
+      }
174
+   }
175
+}
176
+
177
+
178
+// ----------------------------------------------------------------------------
179
+// Input units in inches.
180
+// Note: units of measure in drawing are mm!
181
+module english_thread (diameter=0.25, threads_per_inch=20, length=1,
182
+                      internal=false, n_starts=1, thread_size=-1, groove=false,
183
+                      square=false, rectangle=0, angle=30, taper=0, leadin=0,
184
+                      leadfac=1.0, test=false)
185
+{
186
+   // Convert to mm.
187
+   mm_diameter = diameter*25.4;
188
+   mm_pitch = (1.0/threads_per_inch)*25.4;
189
+   mm_length = length*25.4;
190
+
191
+   echo (str ("mm_diameter: ", mm_diameter));
192
+   echo (str ("mm_pitch: ", mm_pitch));
193
+   echo (str ("mm_length: ", mm_length));
194
+   metric_thread (mm_diameter, mm_pitch, mm_length, internal, n_starts,
195
+                  thread_size, groove, square, rectangle, angle, taper, leadin,
196
+                  leadfac, test);
197
+}
198
+
199
+// ----------------------------------------------------------------------------
200
+module metric_thread_turns (diameter, pitch, length, internal, n_starts,
201
+                            thread_size, groove, square, rectangle, angle,
202
+                            taper)
203
+{
204
+   // Number of turns needed.
205
+   n_turns = floor (length/pitch);
206
+
207
+   intersection () {
208
+
209
+      // Start one below z = 0.  Gives an extra turn at each end.
210
+      for (i=[-1*n_starts : n_turns+1]) {
211
+         translate ([0, 0, i*pitch]) {
212
+            metric_thread_turn (diameter, pitch, internal, n_starts,
213
+                                thread_size, groove, square, rectangle, angle,
214
+                                taper, i*pitch);
215
+         }
216
+      }
217
+
218
+      // Cut to length.
219
+      translate ([0, 0, length/2]) {
220
+         cube ([diameter*3, diameter*3, length], center=true);
221
+      }
222
+   }
223
+}
224
+
225
+
226
+// ----------------------------------------------------------------------------
227
+module metric_thread_turn (diameter, pitch, internal, n_starts, thread_size,
228
+                           groove, square, rectangle, angle, taper, z)
229
+{
230
+   n_segments = segments (diameter);
231
+   fraction_circle = 1.0/n_segments;
232
+   for (i=[0 : n_segments-1]) {
233
+      rotate ([0, 0, i*360*fraction_circle]) {
234
+         translate ([0, 0, i*n_starts*pitch*fraction_circle]) {
235
+            //current_diameter = diameter - taper*(z + i*n_starts*pitch*fraction_circle);
236
+            thread_polyhedron ((diameter - taper*(z + i*n_starts*pitch*fraction_circle))/2,
237
+                               pitch, internal, n_starts, thread_size, groove,
238
+                               square, rectangle, angle);
239
+         }
240
+      }
241
+   }
242
+}
243
+
244
+
245
+// ----------------------------------------------------------------------------
246
+module thread_polyhedron (radius, pitch, internal, n_starts, thread_size,
247
+                          groove, square, rectangle, angle)
248
+{
249
+   n_segments = segments (radius*2);
250
+   fraction_circle = 1.0/n_segments;
251
+
252
+   local_rectangle = rectangle ? rectangle : 1;
253
+
254
+   h = (square || rectangle) ? thread_size*local_rectangle/2 : thread_size / (2 * tan(angle));
255
+   outer_r = radius + (internal ? h/20 : 0); // Adds internal relief.
256
+   //echo (str ("outer_r: ", outer_r));
257
+
258
+   // A little extra on square thread -- make sure overlaps cylinder.
259
+   h_fac1 = (square || rectangle) ? 1.1 : 0.875;
260
+   inner_r = radius - h*h_fac1; // Does NOT do Dmin_truncation - do later with
261
+                                // cylinder.
262
+
263
+   translate_y = groove ? outer_r + inner_r : 0;
264
+   reflect_x   = groove ? 1 : 0;
265
+
266
+   // Make these just slightly bigger (keep in proportion) so polyhedra will
267
+   // overlap.
268
+   x_incr_outer = (! groove ? outer_r : inner_r) * fraction_circle * 2 * PI * 1.02;
269
+   x_incr_inner = (! groove ? inner_r : outer_r) * fraction_circle * 2 * PI * 1.02;
270
+   z_incr = n_starts * pitch * fraction_circle * 1.005;
271
+
272
+   /*
273
+    (angles x0 and x3 inner are actually 60 deg)
274
+
275
+                          /\  (x2_inner, z2_inner) [2]
276
+                         /  \
277
+   (x3_inner, z3_inner) /    \
278
+                  [3]   \     \
279
+                        |\     \ (x2_outer, z2_outer) [6]
280
+                        | \    /
281
+                        |  \  /|
282
+             z          |[7]\/ / (x1_outer, z1_outer) [5]
283
+             |          |   | /
284
+             |   x      |   |/
285
+             |  /       |   / (x0_outer, z0_outer) [4]
286
+             | /        |  /     (behind: (x1_inner, z1_inner) [1]
287
+             |/         | /
288
+    y________|          |/
289
+   (r)                  / (x0_inner, z0_inner) [0]
290
+
291
+   */
292
+
293
+   x1_outer = outer_r * fraction_circle * 2 * PI;
294
+
295
+   z0_outer = (outer_r - inner_r) * tan(angle);
296
+   //echo (str ("z0_outer: ", z0_outer));
297
+
298
+   //polygon ([[inner_r, 0], [outer_r, z0_outer],
299
+   //        [outer_r, 0.5*pitch], [inner_r, 0.5*pitch]]);
300
+   z1_outer = z0_outer + z_incr;
301
+
302
+   // Give internal square threads some clearance in the z direction, too.
303
+   bottom = internal ? 0.235 : 0.25;
304
+   top    = internal ? 0.765 : 0.75;
305
+
306
+   translate ([0, translate_y, 0]) {
307
+      mirror ([reflect_x, 0, 0]) {
308
+
309
+         if (square || rectangle) {
310
+
311
+            // Rule for face ordering: look at polyhedron from outside: points must
312
+            // be in clockwise order.
313
+            polyhedron (
314
+               points = [
315
+                         [-x_incr_inner/2, -inner_r, bottom*thread_size],         // [0]
316
+                         [x_incr_inner/2, -inner_r, bottom*thread_size + z_incr], // [1]
317
+                         [x_incr_inner/2, -inner_r, top*thread_size + z_incr],    // [2]
318
+                         [-x_incr_inner/2, -inner_r, top*thread_size],            // [3]
319
+
320
+                         [-x_incr_outer/2, -outer_r, bottom*thread_size],         // [4]
321
+                         [x_incr_outer/2, -outer_r, bottom*thread_size + z_incr], // [5]
322
+                         [x_incr_outer/2, -outer_r, top*thread_size + z_incr],    // [6]
323
+                         [-x_incr_outer/2, -outer_r, top*thread_size]             // [7]
324
+                        ],
325
+
326
+               faces = [
327
+                         [0, 3, 7, 4],  // This-side trapezoid
328
+
329
+                         [1, 5, 6, 2],  // Back-side trapezoid
330
+
331
+                         [0, 1, 2, 3],  // Inner rectangle
332
+
333
+                         [4, 7, 6, 5],  // Outer rectangle
334
+
335
+                         // These are not planar, so do with separate triangles.
336
+                         [7, 2, 6],     // Upper rectangle, bottom
337
+                         [7, 3, 2],     // Upper rectangle, top
338
+
339
+                         [0, 5, 1],     // Lower rectangle, bottom
340
+                         [0, 4, 5]      // Lower rectangle, top
341
+                        ]
342
+            );
343
+         } else {
344
+
345
+            // Rule for face ordering: look at polyhedron from outside: points must
346
+            // be in clockwise order.
347
+            polyhedron (
348
+               points = [
349
+                         [-x_incr_inner/2, -inner_r, 0],                        // [0]
350
+                         [x_incr_inner/2, -inner_r, z_incr],                    // [1]
351
+                         [x_incr_inner/2, -inner_r, thread_size + z_incr],      // [2]
352
+                         [-x_incr_inner/2, -inner_r, thread_size],              // [3]
353
+
354
+                         [-x_incr_outer/2, -outer_r, z0_outer],                 // [4]
355
+                         [x_incr_outer/2, -outer_r, z0_outer + z_incr],         // [5]
356
+                         [x_incr_outer/2, -outer_r, thread_size - z0_outer + z_incr], // [6]
357
+                         [-x_incr_outer/2, -outer_r, thread_size - z0_outer]    // [7]
358
+                        ],
359
+
360
+               faces = [
361
+                         [0, 3, 7, 4],  // This-side trapezoid
362
+
363
+                         [1, 5, 6, 2],  // Back-side trapezoid
364
+
365
+                         [0, 1, 2, 3],  // Inner rectangle
366
+
367
+                         [4, 7, 6, 5],  // Outer rectangle
368
+
369
+                         // These are not planar, so do with separate triangles.
370
+                         [7, 2, 6],     // Upper rectangle, bottom
371
+                         [7, 3, 2],     // Upper rectangle, top
372
+
373
+                         [0, 5, 1],     // Lower rectangle, bottom
374
+                         [0, 4, 5]      // Lower rectangle, top
375
+                        ]
376
+            );
377
+         }
378
+      }
379
+   }
380
+}
381
+
382
+

+ 390
- 0
hardware/openchrono.scad Переглянути файл

@@ -0,0 +1,390 @@
1
+outer_dia = 55;
2
+inner_dia = 8.5;
3
+height = 100;
4
+
5
+body_gap = 0.1;
6
+
7
+body_screw_off = 10;
8
+body_screw_pos = 20;
9
+body_screw_dia = 3.2;
10
+body_screw_head = 5.8;
11
+body_screw_depth = 3.2;
12
+body_screw_insert_dia = 5.0;
13
+body_screw_insert_height = 15.0;
14
+
15
+lcd_pcb_w = 29.0;
16
+lcd_pcb_h = 29.0;
17
+lcd_pcb_d = 5.2;
18
+lcd_hole_dia = 2.0;
19
+lcd_hole_w = 6.0;
20
+lcd_hole_h = 2.5;
21
+lcd_off = 10.0;
22
+lcd_hole_off_x = 23.1;
23
+lcd_hole_off_y = 23.65;
24
+lcd_hole_off_y_total = 0.4;
25
+lcd_hole_screw_len = 10.0;
26
+
27
+arduino_w = 19.0;
28
+arduino_h = 46.5 + 10;
29
+arduino_d = 10.0;
30
+
31
+bat_w = 11.5;
32
+bat_l = 45.5;
33
+bat_tab_w = 9.5;
34
+bat_tab_h = 8.5;
35
+bat_tab_d = 2.5; // TODO?
36
+bat_tab_con_w = 5.0; // TODO?
37
+bat_tab_con_h = 6.5; // TODO?
38
+bat_spring_w = 7.5; // TODO?
39
+bat_spring_dist = 7.5; // TODO?
40
+bat_wall = 1.0;
41
+bat_angle = 48;
42
+
43
+led_dia = 3.3;
44
+led_l = 4.5;
45
+led_off = 15;
46
+led_ridge_dia = 4.2;
47
+led_ridge_h = 1.6;
48
+
49
+switch_w = 12.0;
50
+switch_h = 6.5;
51
+switch_d = 10.0;
52
+switch_plate_w = 20.5;
53
+switch_plate_h = switch_h;
54
+switch_dia = 2.6;
55
+switch_screw_l = 10.0;
56
+switch_screw_d = 15.0;
57
+switch_off = 15;
58
+
59
+bat_h = bat_l + bat_spring_dist + 2 * bat_wall + 2 * bat_tab_d;
60
+
61
+$fn = 42;
62
+
63
+echo("sensor_distance", height - 2 * led_off);
64
+
65
+// https://dkprojects.net/openscad-threads/ 
66
+include <extern/threads.scad>
67
+
68
+// 1911
69
+thread_profile_1911 = [
70
+    true, // type is_male
71
+    12.0, // diameter
72
+    1.0, // pitch
73
+    0.0, // offset
74
+    9.0 // length
75
+];
76
+
77
+// M14x1.0 female thread
78
+thread_profile_m14 = [
79
+    false, // type is_male
80
+    14.0, // diameter
81
+    1.0, // pitch
82
+    0.0, // offset
83
+    12.0 // length
84
+];
85
+
86
+// ASG / KWC Cobray Ingram M11 CO2 NBB 6mm
87
+thread_profile_mac11 = [
88
+    false, // type is_male
89
+    16.5, // diameter
90
+    1.5, // pitch
91
+    8.0, // offset
92
+    10.0 // length
93
+];
94
+
95
+// debug / testing
96
+thread_profile_none = [ false, 0, 0, 0, 0 ];
97
+
98
+thread_profile = thread_profile_m14;
99
+thread_base = 1.0;
100
+
101
+// how deep things on the outside have to be set in
102
+function circle_offset_deviation(off, dia) =
103
+    dia * (1 - sin(acos(off * 2 / dia))) / 2;
104
+
105
+module lcd_cutout() {
106
+    difference() {
107
+        cube([lcd_pcb_w, lcd_pcb_h, lcd_pcb_d + 10]);
108
+        
109
+        for (x = [0, lcd_pcb_w - lcd_hole_w])
110
+        for (y = [0, lcd_pcb_h - lcd_hole_w])
111
+        translate([x, y, -1])
112
+            cube([lcd_hole_w, lcd_hole_w, lcd_hole_h + 1]);
113
+    }
114
+            
115
+    for (x = [0, lcd_hole_off_x])
116
+    for (y = [0, lcd_hole_off_y])
117
+    translate([x + lcd_hole_w / 2, y + lcd_hole_w / 2 - lcd_hole_off_y_total, lcd_hole_h - lcd_hole_screw_len])
118
+    cylinder(d = lcd_hole_dia, h = lcd_hole_screw_len + 1);
119
+}
120
+
121
+module arduino_cutout() {
122
+    cube([arduino_w, arduino_h, arduino_d]);
123
+}
124
+
125
+module bat_cutout() {
126
+    // battery
127
+    translate([0, 0, bat_tab_d + bat_wall])
128
+    cube([bat_w, bat_w, bat_l + bat_spring_dist]);
129
+    
130
+    // negative terminal
131
+    for (z = [0, bat_l + bat_spring_dist + 2 * bat_wall + bat_tab_d])
132
+    translate([(bat_w - bat_tab_w) / 2, (bat_w - bat_tab_h) / 2, z])
133
+    cube([bat_tab_w, bat_tab_h + (bat_w - bat_tab_h) / 2, bat_tab_d]);
134
+
135
+    // spring
136
+    for (z = [bat_tab_d, bat_l + bat_spring_dist + bat_wall + bat_tab_d])
137
+    translate([(bat_w - bat_spring_w) / 2, (bat_w - bat_spring_w) / 2, z - 0.1])
138
+    cube([bat_spring_w, bat_spring_w + (bat_w - bat_spring_w) / 2, bat_wall + 0.2]);
139
+}
140
+
141
+module switch_cutout() {
142
+    translate([-switch_w / 2, -10, -switch_h / 2])
143
+    cube([switch_w, switch_d + 10, switch_h]);
144
+    
145
+    translate([-switch_plate_w / 2, -switch_d, -switch_plate_h / 2])
146
+    cube([switch_plate_w, 10, switch_plate_h]);
147
+    
148
+    for (x = [1, -1])
149
+    scale([x, 1, 1])
150
+    translate([-switch_screw_d / 2, -10, 0])
151
+    rotate([-90, 0, 0])
152
+    cylinder(d = switch_dia, h = switch_screw_l + 10);
153
+}
154
+
155
+module thread(profile, thread_draw) {
156
+    if (profile[0]) {
157
+        // male thread
158
+        difference() {
159
+            union() {
160
+                cylinder(d = outer_dia, h = thread_base);
161
+                metric_thread(profile[1], profile[2], profile[4] + thread_base, test=!thread_draw);
162
+            }
163
+            
164
+            translate([0, 0, -1])
165
+            cylinder(d = inner_dia, h = profile[4] + thread_base + 2);
166
+        }
167
+    } else {
168
+        // female thread
169
+        difference() {
170
+            cylinder(d = outer_dia, h = thread_base + profile[4] + profile[3]);
171
+            
172
+            metric_thread(profile[1], profile[2], profile[4] + thread_base + 1, true, test=!thread_draw);
173
+            
174
+            translate([0, 0, thread_base + profile[4]])
175
+            cylinder(d = profile[1] + 2, h = profile[3] + 1);
176
+            
177
+            translate([0, 0, -1])
178
+            cylinder(d = inner_dia, h = profile[4] + thread_base + profile[3] + 2);
179
+        } 
180
+    }
181
+}
182
+
183
+module half_body(right_side) {
184
+    difference() {
185
+        // body
186
+        cylinder(d = outer_dia, h = height);
187
+        
188
+        // inner tube
189
+        translate([0, 0, -1])
190
+        cylinder(d = inner_dia, h = height + 2);
191
+        
192
+        // remove half of cylinder
193
+        translate([-outer_dia / 2 - 1, -outer_dia + body_gap / 2, -1])
194
+        cube([outer_dia + 2, outer_dia, height + 2]);
195
+        
196
+        // led cutouts
197
+        for (x = [1, -1])
198
+        scale([x, 1, 1])
199
+        for (z = [led_off, height - led_off])
200
+        translate([inner_dia / 2 - 1, 0, z])
201
+        rotate([0, 90, 0]) {
202
+            cylinder(d = led_dia, h = led_l + led_ridge_h + 1);
203
+            
204
+            translate([0, 0, led_l + 1])
205
+            cylinder(d = led_ridge_dia, h = led_ridge_h);
206
+        }
207
+        
208
+        // TODO hacky sensor cable, arduino side
209
+        for (z = [1, -1])
210
+        translate([0, 0, height / 2])
211
+        scale([right_side ? -1 : 1, 1, z])
212
+        translate([0, 0, height / 2 - led_off])
213
+        hull() {
214
+            for (x = [0, 5])
215
+            translate([-inner_dia / 2 - led_l - led_ridge_h - x, 0, 0])
216
+            rotate([0, 90, 0])
217
+            cylinder(d = led_ridge_dia, h = led_ridge_h);
218
+            
219
+            translate([-inner_dia / 2 - led_l - led_ridge_h - 5, 0, -10])
220
+            cylinder(d = led_ridge_dia, h = led_ridge_h);
221
+        }
222
+        
223
+        // TODO hacky led cable, led side
224
+        for (z = [1, -1])
225
+        translate([0, 0, height / 2])
226
+        scale([right_side ? 1 : -1, 1, z])
227
+        translate([0, 0, height / 2 - led_off])
228
+        hull() {
229
+            for (x = [0, 5])
230
+            translate([-inner_dia / 2 - led_l - led_ridge_h - x, 0, 0])
231
+            rotate([0, 90, 0])
232
+            cylinder(d = led_ridge_dia, h = led_ridge_h);
233
+            
234
+            translate([-inner_dia / 2 - led_l - led_ridge_h - 5, 0, -height / 2 + led_off - 1])
235
+            cube([led_ridge_dia + 2, led_ridge_dia - 2, led_ridge_h]);
236
+        }
237
+    }
238
+}
239
+
240
+module screw_holes(with_head) {
241
+    for (x = [body_screw_pos, -body_screw_pos])
242
+    for (z = [body_screw_off, height - body_screw_off])
243
+    translate([x, 0, z])
244
+    rotate([-90, 0, 0]) {
245
+        translate([0, 0, -1])
246
+        if (with_head)
247
+        cylinder(d = body_screw_dia, h = outer_dia / 2 + 2);
248
+        else
249
+        cylinder(d = body_screw_insert_dia, h = body_screw_insert_height);
250
+        
251
+        if (with_head)
252
+        translate([0, 0, outer_dia / 2 - circle_offset_deviation(body_screw_pos + body_screw_head / 2, outer_dia) - body_screw_depth - 2])
253
+        cylinder(d = body_screw_head, h = 50);
254
+    }
255
+}
256
+
257
+module left_half(thread_draw) {
258
+    difference() {
259
+        union() {
260
+            half_body(false);
261
+            
262
+            translate([0, 0, height])
263
+            thread(thread_profile, thread_draw);
264
+        }
265
+        
266
+        translate([-outer_dia / 2 - 1, -outer_dia / 2 - 1 + body_gap / 2, height - 1])
267
+        cube([outer_dia + 2, outer_dia / 2 + 1, 50]);
268
+        
269
+        screw_holes(false);
270
+        
271
+        translate([0, outer_dia / 2 - circle_offset_deviation(lcd_pcb_h / 2, outer_dia) - lcd_pcb_d, height / 2 + lcd_off])
272
+        rotate([0, 90, 0])
273
+        translate([lcd_pcb_w / 2, 0, -lcd_pcb_h / 2])
274
+        rotate([90, 0, 180])
275
+        lcd_cutout();
276
+        
277
+        translate([-outer_dia / 2 + ((outer_dia / 2) - (inner_dia / 2) - arduino_w) / 2, arduino_d / 2, -arduino_h / 2 + height / 2])
278
+        rotate([90, 0, 0])
279
+        arduino_cutout();
280
+        
281
+        translate([0, outer_dia / 2 - circle_offset_deviation(switch_plate_w / 2, outer_dia), height / 2 - switch_off])
282
+        rotate([0, 0, 180])
283
+        switch_cutout();
284
+        
285
+        // TODO hacky switch cable
286
+        translate([-16, -10, height / 2 - switch_off])
287
+        rotate([-90, 0, -27])
288
+        cylinder(d = switch_h - 2, h = outer_dia);
289
+        
290
+        // TODO hacky lcd cable
291
+        translate([-15, -10, 60])
292
+        rotate([-90, 0, -12])
293
+        cylinder(d = 6.0, h = outer_dia);
294
+        
295
+        // TODO hacky led cable
296
+        translate([0, 0, height / 2 + 3]) {
297
+            translate([inner_dia / 2 + led_l + led_ridge_dia / 2, led_ridge_h + inner_dia / 2 + 2, 0])
298
+            rotate([90, 0, 0])
299
+            cylinder(d = led_ridge_dia, h = led_ridge_h + inner_dia / 2 + 2);
300
+            
301
+            hull() {
302
+                translate([inner_dia / 2 + led_l + led_ridge_dia / 2, led_ridge_h + inner_dia / 2 + 2, 0])
303
+                rotate([90, 0, 0])
304
+                cylinder(d = led_ridge_dia, h = led_ridge_h);
305
+            
306
+                translate([inner_dia / 2 + led_l + led_ridge_dia / 2 - 5, led_ridge_h + inner_dia / 2 + 2, 0])
307
+                rotate([0, 90, 0])
308
+                cylinder(d = led_ridge_dia, h = led_ridge_h + 5);
309
+            }
310
+            
311
+            translate([inner_dia / 2 + led_l + led_ridge_dia / 2 - 25, led_ridge_h + inner_dia / 2 + 2, 0])
312
+            rotate([0, 90, 0])
313
+            cylinder(d = led_ridge_dia, h = led_ridge_h + 25);
314
+            
315
+            hull() {
316
+                translate([inner_dia / 2 + led_l + led_ridge_dia / 2 - 25, led_ridge_h + inner_dia / 2 + 2, 0])
317
+                rotate([90, 0, 0])
318
+                cylinder(d = led_ridge_dia, h = led_ridge_h + 10);
319
+            
320
+                translate([inner_dia / 2 + led_l + led_ridge_dia / 2 - 25, led_ridge_h + inner_dia / 2 + 2, 0])
321
+                rotate([0, 90, 0])
322
+                cylinder(d = led_ridge_dia, h = led_ridge_h);
323
+            }
324
+        }
325
+    }
326
+}
327
+
328
+module right_half(thread_draw) {
329
+    difference() {
330
+        union() {
331
+            half_body(true);
332
+            
333
+            translate([0, 0, height])
334
+            rotate([0, 0, 180])
335
+            thread(thread_profile, thread_draw);
336
+        }
337
+        
338
+        translate([-outer_dia / 2 - 1, -outer_dia / 2 - 1 + body_gap / 2, height - 1])
339
+        cube([outer_dia + 2, outer_dia / 2 + 1, 50]);
340
+        
341
+        screw_holes(true);
342
+        
343
+        translate([outer_dia / 2 - arduino_w -((outer_dia / 2) - (inner_dia / 2) - arduino_w) / 2, arduino_d / 2, -arduino_h / 2 + height / 2])
344
+        rotate([90, 0, 0])
345
+        arduino_cutout();
346
+        
347
+        for (a = [0, bat_angle, -bat_angle])
348
+        rotate([0, 0, a])
349
+        translate([-bat_w / 2, outer_dia / 2 - bat_w, (height - bat_h) / 2])
350
+        bat_cutout();
351
+    }
352
+}
353
+
354
+module assembly_closed(thread_draw) {
355
+    right_half(thread_draw);
356
+    
357
+    rotate([0, 0, 180])
358
+    left_half(thread_draw);
359
+}
360
+
361
+module assembly_opened(angle, thread_draw) {
362
+    translate([-outer_dia / 2, 0, 0]) {
363
+        rotate([0, 0, angle / 2])
364
+        translate([outer_dia / 2, 0, 0])
365
+        right_half(thread_draw);
366
+        
367
+        rotate([0, 0, -angle / 2])
368
+        translate([outer_dia / 2, 0, 0])
369
+        rotate([0, 0, 180])
370
+        left_half(thread_draw);
371
+    }
372
+}
373
+
374
+module print() {
375
+    translate([outer_dia / 2 + 5, 0, 0])
376
+    left_half(true);
377
+    
378
+    translate([-outer_dia / 2 - 5, 0, 0])
379
+    right_half(true);
380
+}
381
+
382
+//lcd_cutout();
383
+
384
+//left_half(false);
385
+//right_half(false);
386
+
387
+//assembly_closed(false);
388
+//assembly_opened(90, false);
389
+
390
+print();

Завантаження…
Відмінити
Зберегти