├── .gitignore
├── 3d_print_case
├── badger2040w_box.stl
└── box.scad
├── Charts
├── data.csv
├── data2.csv
└── heatmap.py
├── Clock_Stuff
├── cl1.py
├── cl2.py
├── icon-cl1.jpg
└── icon-cl2.jpg
├── LICENSE
├── README.md
├── Wifi_Toggle.py
├── ahtx0.py
├── apps_provisioning
├── apps.py
└── icon-apps.jpg
├── data
└── totp_keys.json
├── examples
├── apps.py
├── badge.py
├── cl1.py
├── cl2.py
├── clock.py
├── dash.py
├── ebook.py
├── fonts.py
├── form.py
├── help.py
├── icon-apps.jpg
├── icon-badge.jpg
├── icon-cl1.jpg
├── icon-cl2.jpg
├── icon-clock.jpg
├── icon-dash.jpg
├── icon-ebook.jpg
├── icon-fonts.jpg
├── icon-form.jpg
├── icon-help.jpg
├── icon-image.jpg
├── icon-info.jpg
├── icon-list.jpg
├── icon-logger.jpg
├── icon-net-info.jpg
├── icon-news.jpg
├── icon-power.jpg
├── icon-qrgen.jpg
├── icon-sendODK.jpg
├── icon-space.jpg
├── icon-totp.jpg
├── icon-totp2.jpg
├── icon-weather.jpg
├── image.py
├── info.py
├── list.py
├── logger.py
├── net_info.py
├── news.py
├── power.py
├── qrgen.py
├── sendODK.py
├── space.py
├── totp.py
├── totp2.py
└── weather.py
├── forms
└── Badger 2040 Test.odkbuild
├── icons
├── a.jpg
├── angry.jpg
├── b.jpg
├── c.jpg
├── d.jpg
├── happy.jpg
├── icon-cloud.jpg
├── icon-cloud_dark.jpg
├── icon-rain.jpg
├── icon-rain_dark.jpg
├── icon-snow.jpg
├── icon-snow_dark.jpg
├── icon-storm.jpg
├── icon-storm_dark.jpg
├── icon-sun.jpg
├── icon-sun_dark.jpg
├── joyful.jpg
├── neutral.jpg
└── sad.jpg
├── img
├── 3d_print_case.png
├── 3d_print_case_2.jpeg
├── apps_provision_01.jpg
├── apps_provision_02.jpg
├── authenticator.jpg
├── barchart.jpg
├── clk1.png
├── clk2.png
├── dash.jpeg
├── heatmap_matrix.jpg
├── heatmap_summary.jpg
├── logger_1.jpeg
├── logger_2.jpeg
├── space.jpeg
└── weather.png
├── lib
└── ahtx0.py
└── provisioning_manifest.json
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 | forms/Badger-2040-Test-Acquire.xlsx
4 | forms/Badger_2040_Central_Store.xlsx
5 | /ChR_Backup
6 |
--------------------------------------------------------------------------------
/3d_print_case/box.scad:
--------------------------------------------------------------------------------
1 | difference(){
2 | union(){
3 | difference(){
4 | cube([86,49,12]);
5 | translate([2,2,2]) cube([82,45,12]);
6 | }
7 | //add cylinders for screwholes
8 | translate([3,3,0])cylinder(d=6,h=12);
9 | translate([3,46,0])cylinder(d=6,h=12);
10 | translate([83,3,0])cylinder(d=6,h=12);
11 | translate([83,46,0])cylinder(d=6,h=12);
12 | }
13 |
14 | //add screwholes
15 | translate([2.5,2.5,0])cylinder(d=2.5,h=12);
16 | translate([2.5,46,0])cylinder(d=2.5,h=12);
17 | translate([83,2.5,0])cylinder(d=2.5,h=12);
18 | translate([83,46,0])cylinder(d=2.5,h=12);
19 |
20 | //add usb port
21 | translate([83,12,8])cube([10,12,4]);
22 |
23 | //thin wall for RPI2040w
24 | translate([80,4.5,2])cube([5,30,14]);
25 |
26 | //thin wall for stuff on the left
27 | translate([0,4.5,2])cube([7,40,14]);
28 |
29 |
30 | }
31 |
32 | //add a side panel to cover the left side
33 | translate([-2,0,0])cube([2,49,12]);
34 |
--------------------------------------------------------------------------------
/Charts/data.csv:
--------------------------------------------------------------------------------
1 | x,y,z
2 | 1,1,1
3 | 1,2,2
4 | 1,3,3
5 | 1,4,4
6 | 1,5,5
7 | 1,6,6
8 | 1,7,7
9 | 1,8,8
10 | 1,9,9
11 | 1,10,10
12 | 2,1,2
13 | 2,2,4
14 | 2,3,6
15 | 2,4,8
16 | 2,5,10
17 | 2,6,12
18 | 2,7,14
19 | 2,8,16
20 | 2,9,18
21 | 2,10,20
22 | 3,1,3
23 | 3,2,6
24 | 3,3,9
25 | 3,4,12
26 | 3,5,15
27 | 3,6,18
28 | 3,7,21
29 | 3,8,24
30 | 3,9,27
31 | 3,10,30
32 | 4,1,4
33 | 4,2,8
34 | 4,3,12
35 | 4,4,16
36 | 4,5,20
37 | 4,6,24
38 | 4,7,28
39 | 4,8,32
40 | 4,9,36
41 | 4,10,40
42 | 5,1,5
43 | 5,2,10
44 | 5,3,15
45 | 5,4,20
46 | 5,5,25
47 | 5,6,30
48 | 5,7,35
49 | 5,8,40
50 | 5,9,45
51 | 5,10,50
52 | 6,1,6
53 | 6,2,12
54 | 6,3,18
55 | 6,4,24
56 | 6,5,30
57 | 6,6,36
58 | 6,7,42
59 | 6,8,48
60 | 6,9,54
61 | 6,10,60
62 | 7,1,7
63 | 7,2,14
64 | 7,3,21
65 | 7,4,28
66 | 7,5,35
67 | 7,6,42
68 | 7,7,49
69 | 7,8,56
70 | 7,9,63
71 | 7,10,70
72 | 8,1,8
73 | 8,2,16
74 | 8,3,24
75 | 8,4,32
76 | 8,5,40
77 | 8,6,48
78 | 8,7,56
79 | 8,8,64
80 | 8,9,72
81 | 8,10,80
82 | 9,1,9
83 | 9,2,18
84 | 9,3,27
85 | 9,4,36
86 | 9,5,45
87 | 9,6,54
88 | 9,7,63
89 | 9,8,72
90 | 9,9,81
91 | 9,10,90
92 | 10,1,10
93 | 10,2,20
94 | 10,3,30
95 | 10,4,40
96 | 10,5,50
97 | 10,6,60
98 | 10,7,70
99 | 10,8,80
100 | 10,9,90
101 | 10,10,100
102 |
--------------------------------------------------------------------------------
/Charts/data2.csv:
--------------------------------------------------------------------------------
1 | x,y,z
2 | 0,36,3
3 | 0,35,0
4 | 0,24,1
5 | 0,36,0
6 | 0,6,12
7 | 1,65,2
8 | 1,53,7
9 | 1,42,12
10 | 1,7,10
11 | 1,43,5
12 | 1,48,13
13 | 1,60,13
14 | 1,1,13
15 | 2,51,6
16 | 2,36,14
17 | 2,42,6
18 | 2,6,15
19 | 2,68,8
20 | 2,69,12
21 | 2,67,14
22 | 2,28,3
23 | 2,52,8
24 | 2,31,8
25 | 2,23,6
26 | 3,33,3
27 | 3,4,11
28 | 3,5,11
29 | 3,14,12
30 | 3,31,8
31 | 4,8,5
32 | 4,20,7
33 | 4,28,15
34 | 4,49,4
35 | 4,67,13
36 | 4,63,4
37 | 4,60,3
38 | 5,60,2
39 | 5,5,15
40 | 5,1,8
41 | 5,26,6
42 | 6,35,6
43 | 6,45,7
44 | 6,32,5
45 | 6,7,5
46 | 7,43,13
47 | 7,36,0
48 | 7,4,14
49 | 7,70,7
50 | 7,58,0
51 | 7,3,11
52 | 8,34,14
53 | 8,50,4
54 | 8,66,5
55 | 8,51,12
56 | 8,61,3
57 | 8,27,4
58 | 8,44,12
59 | 8,48,2
60 | 9,35,12
61 | 9,55,1
62 | 9,11,4
63 | 9,53,0
64 | 9,56,8
65 | 9,21,15
66 | 10,44,15
67 | 10,65,10
68 | 10,46,14
69 | 10,64,2
70 | 10,20,12
71 | 10,46,5
72 | 10,1,8
73 | 11,43,6
74 | 11,44,6
75 | 11,9,7
76 | 11,41,7
77 | 11,14,15
78 | 12,30,4
79 | 12,22,0
80 | 12,51,15
81 | 12,4,4
82 | 12,64,3
83 | 12,51,12
84 | 12,12,12
85 | 12,38,3
86 | 13,53,7
87 | 13,17,4
88 | 13,48,0
89 | 13,30,1
90 | 13,9,4
91 | 14,58,12
92 | 14,69,7
93 | 14,52,7
94 | 14,60,13
95 | 14,4,8
96 | 14,29,9
97 | 14,64,13
98 | 15,15,10
99 | 15,45,2
100 | 15,51,1
101 | 15,45,15
102 | 16,18,3
103 | 16,42,9
104 | 16,34,14
105 | 16,64,5
106 | 16,52,12
107 | 17,15,4
108 | 17,41,14
109 | 17,39,15
110 | 17,42,5
111 | 17,10,14
112 | 17,70,10
113 | 17,3,9
114 | 18,22,1
115 | 18,54,5
116 | 18,1,9
117 | 18,4,2
118 | 18,11,5
119 | 18,46,6
120 | 18,49,7
121 | 19,63,12
122 | 19,5,14
123 | 19,54,14
124 | 19,52,1
125 | 20,54,15
126 | 20,32,10
127 | 20,39,4
128 | 20,12,1
129 | 20,40,12
130 | 20,36,4
131 | 20,4,7
132 | 20,28,15
133 | 20,51,4
134 | 21,37,12
135 | 21,69,5
136 | 21,28,4
137 | 21,22,3
138 | 21,13,13
139 | 22,68,5
140 | 22,12,15
141 | 22,24,14
142 | 22,65,15
143 | 22,3,2
144 | 22,27,0
145 | 23,0,8
146 | 23,42,3
147 | 23,66,13
148 | 23,60,10
149 | 24,13,5
150 | 24,15,12
151 | 25,53,13
152 | 25,55,1
153 | 25,48,0
154 | 25,33,2
155 | 25,30,3
156 | 25,40,15
157 | 25,23,9
158 | 25,47,1
159 | 26,42,7
160 | 26,67,2
161 | 26,55,13
162 | 26,68,12
163 | 26,37,0
164 | 26,64,8
165 | 27,32,10
166 | 27,31,11
167 | 27,29,4
168 | 28,48,6
169 | 28,35,3
170 | 29,8,4
171 | 29,24,2
172 | 29,64,3
173 | 29,48,4
174 | 29,1,12
175 | 30,18,3
176 | 30,54,4
177 | 30,2,5
178 | 30,41,13
179 | 30,47,5
180 | 30,24,2
181 | 30,35,5
182 | 30,34,7
183 | 31,62,14
184 | 31,49,7
185 | 32,55,1
186 | 32,42,10
187 | 32,14,8
188 | 32,16,4
189 | 32,51,13
190 | 32,29,12
191 | 33,50,9
192 | 33,55,1
193 | 34,23,2
194 | 35,66,0
195 | 35,2,2
196 | 35,41,11
197 | 35,61,6
198 | 35,43,11
199 | 35,38,0
200 | 35,46,0
201 | 35,62,15
202 | 35,48,13
203 | 35,15,8
204 | 35,18,6
205 | 36,47,0
206 | 37,39,15
207 | 37,55,2
208 | 37,25,8
209 | 37,23,3
210 | 37,5,5
211 | 37,15,10
212 | 37,9,12
213 | 38,53,10
214 | 38,19,14
215 | 38,34,12
216 | 38,43,3
217 | 38,20,14
218 | 38,27,7
219 | 38,66,10
220 | 39,21,9
221 | 39,61,9
222 | 39,55,15
223 | 40,7,1
224 | 40,68,2
225 | 40,37,6
226 | 40,68,12
227 | 40,16,2
228 | 40,62,5
229 | 40,26,8
230 | 40,46,6
231 | 40,47,8
232 | 40,4,10
233 | 41,15,15
234 | 41,27,15
235 | 41,28,13
236 | 41,70,1
237 | 41,58,1
238 | 41,43,7
239 | 41,50,4
240 | 41,41,0
241 | 41,70,11
242 | 42,0,8
243 | 42,15,5
244 | 42,3,7
245 | 42,17,6
246 | 42,52,8
247 | 42,68,5
248 | 42,52,3
249 | 42,63,6
250 | 42,53,4
251 | 43,44,13
252 | 43,14,14
253 | 43,52,11
254 | 43,21,11
255 | 43,63,13
256 | 43,7,4
257 | 43,50,0
258 | 43,58,6
259 | 43,6,5
260 | 43,42,10
261 | 43,69,5
262 | 43,8,5
263 | 43,27,13
264 | 44,16,12
265 | 44,38,13
266 | 44,37,5
267 | 44,50,10
268 | 44,69,10
269 | 44,55,3
270 | 44,65,6
271 | 45,31,3
272 | 45,10,15
273 | 45,66,15
274 | 45,5,1
275 | 45,45,5
276 | 46,50,12
277 | 46,35,9
278 | 46,55,4
279 | 46,20,7
280 | 46,42,5
281 | 46,2,12
282 | 47,45,14
283 | 47,44,5
284 | 47,41,6
285 | 47,66,12
286 | 47,59,10
287 | 47,44,15
288 | 47,62,8
289 | 48,15,1
290 | 48,31,15
291 | 48,39,2
292 | 48,19,6
293 | 48,44,11
294 | 48,32,10
295 | 49,0,5
296 | 50,0,2
297 | 50,27,6
298 | 50,41,0
299 | 50,47,2
300 | 50,28,7
301 | 50,55,8
302 |
--------------------------------------------------------------------------------
/Charts/heatmap.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | import math
3 | from badger2040 import WIDTH
4 | from badger2040 import HEIGHT
5 |
6 | ##########################################################################################
7 | # Display Setup
8 | ##########################################################################################
9 |
10 | display = badger2040.Badger2040()
11 | display.led(128)
12 | display.set_update_speed(2)
13 |
14 | ##########################################################################################
15 | # Define function that reads CSV files, with up to three variables specified in the columns
16 | ##########################################################################################
17 |
18 | def read_csv(filename, x_name, y_name=None, z_name=None):
19 | data = {
20 | x_name: []
21 | }
22 |
23 | if y_name:
24 | data[y_name] = []
25 | if z_name:
26 | data[z_name] = []
27 | # respect the double quote rule about CSV file
28 | def split_line_respecting_quotes(line):
29 | parts = []
30 | temp = ""
31 | inside_quotes = False
32 |
33 | for char in line:
34 | if char == '"':
35 | inside_quotes = not inside_quotes
36 | elif char == ',' and not inside_quotes:
37 | parts.append(temp.strip())
38 | temp = ""
39 | else:
40 | temp += char
41 | if temp: # Append the last field
42 | parts.append(temp.strip())
43 |
44 | return parts
45 |
46 | with open(filename, 'r') as file:
47 | lines = file.readlines()
48 | headers = split_line_respecting_quotes(lines.pop(0).strip())
49 |
50 | if x_name not in headers:
51 | raise ValueError(f"{x_name} column not found in the CSV file.")
52 | x_index = headers.index(x_name)
53 |
54 | y_index = headers.index(y_name) if y_name and y_name in headers else None
55 | z_index = headers.index(z_name) if z_name and z_name in headers else None
56 |
57 | for line_num, line in enumerate(lines, start=2): # Starting from 2 because we removed the header
58 | values = split_line_respecting_quotes(line.strip())
59 |
60 | if len(values) <= x_index:
61 | print(f"Warning: Line {line_num} has incomplete data: {line}")
62 | continue
63 |
64 | try:
65 | data[x_name].append(float(values[x_index].replace('"', '')) if values[x_index] != "" else None)
66 | except ValueError:
67 | print(f"Error at line {line_num}: Cannot convert {values[x_index]} to float for {x_name}")
68 | data[x_name].append(None)
69 |
70 | if y_name and len(values) > y_index:
71 | try:
72 | data[y_name].append(float(values[y_index].replace('"', '')) if values[y_index] != "" else None)
73 | except ValueError:
74 | print(f"Error at line {line_num}: Cannot convert {values[y_index]} to float for {y_name}")
75 | data[y_name].append(None)
76 |
77 | if z_name and len(values) > z_index:
78 | try:
79 | data[z_name].append(float(values[z_index].replace('"', '')) if values[z_index] != "" else None)
80 | except ValueError:
81 | print(f"Error at line {line_num}: Cannot convert {values[z_index]} to float for {z_name}")
82 | data[z_name].append(None)
83 |
84 | return data
85 |
86 |
87 |
88 |
89 |
90 | ##########################################################################################
91 | # Define a function that bins numerical data according to your specified number of bins
92 | ##########################################################################################
93 | def bin_data(values, bin_count=100):
94 | min_val = min(v for v in values if v is not None)
95 | max_val = max(v for v in values if v is not None)
96 | bin_size = (max_val - min_val) / (bin_count - 1) # adjust bin_size for one less bin_count
97 |
98 | # Function to determine which bin a value belongs to
99 | def find_bin(value):
100 | if value is None:
101 | return None
102 | return 1 + int((value - min_val) / bin_size)
103 |
104 | binned_values = [find_bin(value) for value in values]
105 |
106 | # Making sure no value is greater than bin_count
107 | binned_values = [None if value is None else min(bin_count, value) for value in binned_values]
108 |
109 | return binned_values
110 |
111 |
112 | ##########################################################################################
113 | # Define a function that clears the screen and prints a header row
114 | ##########################################################################################
115 | def clear():
116 | display.set_pen(15)
117 | display.clear()
118 | display.set_pen(0)
119 | display.set_font("bitmap8")
120 | display.set_pen(0)
121 | display.rectangle(0, 0, WIDTH, 10)
122 | display.set_pen(15)
123 | display.text("Badger charts", 10, 1, WIDTH, 0.6) # parameters are left padding, top padding, width of screen area, font size
124 | display.set_pen(0)
125 |
126 | ##########################################################################################
127 | # Define a function that draws a heatmap
128 | # This bins x and y in to a user specified number of groups
129 | # Then prints the data as z (pen colour), also binned in to up to 15 levels
130 | # Print colour is always as dark as possible
131 | #
132 | ##########################################################################################
133 |
134 | def plot_heatmap_binned(filename, x_name, y_name, z_name,AXIS_THICKNESS = 3,TICK_SPACING = 10,TICK_LENGTH = 5,x_offset = 30,y_offset = -10,rect_size_x = 4,rect_size_y = 4,x_bins_number = 50,y_bins_number = 30,z_bins_number = 10, skip = 5):
135 | csv_data = read_csv(filename, x_name, y_name, z_name)
136 |
137 | x = csv_data[x_name]
138 | y = csv_data[y_name]
139 | z = csv_data[z_name]
140 |
141 | binned_x = [x_val * rect_size_x for x_val in bin_data(x, x_bins_number)]
142 | binned_y = [y_val * rect_size_y for y_val in bin_data(y, y_bins_number)]
143 | binned_z = bin_data(z, z_bins_number)
144 |
145 | # Dictionaries to store the sum of z values and count of data points
146 | z_sums = {}
147 | counts = {}
148 |
149 | # Iterate and aggregate
150 | for x_val, y_val, z_val in zip(binned_x, binned_y, z):
151 | if None not in (x_val, y_val, z_val):
152 | if (x_val, y_val) in z_sums:
153 | z_sums[(x_val, y_val)] += z_val
154 | counts[(x_val, y_val)] += 1
155 | else:
156 | z_sums[(x_val, y_val)] = z_val
157 | counts[(x_val, y_val)] = 1
158 |
159 | # Calculate average z values
160 | avg_z = {}
161 | for key, value in z_sums.items():
162 | avg_z[key] = value / counts[key]
163 |
164 | filtered_data = [(x_val, y_val, z_val) for x_val, y_val, z_val in zip(binned_x, binned_y, binned_z) if None not in (x_val, y_val, z_val)]
165 |
166 | x_origin = min(binned_x)
167 | y_origin = HEIGHT - min(binned_y)
168 |
169 | x_endpoint = max(binned_x)
170 | y_endpoint = HEIGHT - max(binned_y)
171 |
172 | display.line(x_origin + x_offset, y_origin + y_offset, x_endpoint + x_offset, y_origin + y_offset, AXIS_THICKNESS)
173 | display.line(x_origin + x_offset, y_origin + y_offset, x_origin + x_offset, y_endpoint + y_offset, AXIS_THICKNESS)
174 |
175 | for i in range(0, int((x_endpoint - x_origin) / TICK_SPACING) + 1):
176 | if i % skip == 0:
177 | x_pos = x_origin + (i * TICK_SPACING)
178 | display.line(x_pos + x_offset, y_origin + y_offset + (AXIS_THICKNESS*2), x_pos + x_offset, y_origin - TICK_LENGTH + y_offset + (AXIS_THICKNESS*2), 1)
179 | display.text(str(i), x_pos + x_offset, y_origin + y_offset + TICK_LENGTH + 5, 1, 1)
180 |
181 | y_axis_length = abs(y_origin - y_endpoint)
182 | num_ticks = y_axis_length // TICK_SPACING
183 |
184 | for i in range(num_ticks + 1):
185 | if i % skip == 0:
186 | y_tick_pos = y_origin - (i * TICK_SPACING)
187 | flipped_y = y_tick_pos + y_offset
188 | display.line(x_origin + x_offset, flipped_y, x_origin + x_offset - TICK_LENGTH, flipped_y, 1)
189 | display.text(str(i), x_origin + x_offset - TICK_LENGTH - 20, flipped_y, 1, 1)
190 |
191 | for (x_val, y_val), z_val in avg_z.items():
192 | display.set_pen(int(round(15 - z_val)))
193 | flipped_y = HEIGHT - y_val - rect_size_y
194 | display.rectangle(x_val + x_offset, flipped_y + y_offset, rect_size_x, rect_size_y)
195 |
196 | # Draw legend
197 | max_z = max([z for (_, _, z) in filtered_data])
198 | min_z = min([z for (_, _, z) in filtered_data])
199 |
200 | legend_steps = 5 # For example, you can adjust this
201 | legend_width = 20 # Width of the legend box
202 | legend_height = 10 # Height of each step in the legend
203 |
204 | legend_x_start = x_endpoint + x_offset + 40 # Position legend a bit to the right of the heatmap
205 | legend_y_start = y_origin + y_offset - 10 # Just above the x-axis
206 |
207 | for step in range(legend_steps):
208 | z_val = min_z + (max_z - min_z) * (step / (legend_steps - 1))
209 | display.set_pen(int(round(15 - z_val)))
210 | y_pos = legend_y_start - step * legend_height
211 | display.rectangle(legend_x_start, y_pos, legend_width, legend_height)
212 | display.set_pen(0)
213 | display.text(str(round(z_val, 2)), legend_x_start + legend_width + 5, y_pos, 1, 1)
214 |
215 |
216 | ##########################################################################################
217 | # Define a function that draws a heatmap
218 | # This just draws the raw values of x and y, albeit rounded to the nearest integer
219 | # Then prints the data as z (pen colour), also binned in to up to 15 levels
220 | # Print colour is always as dark as possible
221 | #
222 | # Note that this is probably only useful when you have a single data point for each value of x and y
223 | # Otherwise you'll be printing boxes over boxes
224 | ##########################################################################################
225 | def plot_heatmap_rounded(filename, x_name, y_name, z_name, AXIS_THICKNESS=3, TICK_SPACING=10, TICK_LENGTH=5, x_offset=30, y_offset=-10, rect_size_x=4, rect_size_y=4, z_bins_number=10, skip=5):
226 | csv_data = read_csv(filename, x_name, y_name, z_name)
227 |
228 | x = csv_data[x_name]
229 | y = csv_data[y_name]
230 | z = csv_data[z_name]
231 |
232 | for val in x:
233 | if val is not None and (math.isnan(val) or math.isinf(val)):
234 | print("Found problematic X:", val)
235 |
236 | for val in y:
237 | if val is not None and (math.isnan(val) or math.isinf(val)):
238 | print("Found problematic Y:", val)
239 | print("Unique types in x:", {type(val) for val in x})
240 | print("Unique types in y:", {type(val) for val in y})
241 |
242 | def safe_round(val):
243 | try:
244 | return int(round(val))
245 | except TypeError as e:
246 | print(f"Error when processing value {val} of type {type(val)}. Error: {e}")
247 | return None
248 |
249 | rounded_x = [safe_round(x_val) for x_val in x]
250 | rounded_y = [safe_round(y_val) for y_val in y]
251 | # Round x and y values to the nearest integer
252 | rounded_x = [int(round(x_val)) if x_val is not None and not (math.isnan(x_val) or math.isinf(x_val)) else None for x_val in x]
253 | rounded_y = [int(round(y_val)) if y_val is not None and not (math.isnan(y_val) or math.isinf(y_val)) else None for y_val in y]
254 | binned_z = bin_data(z, z_bins_number)
255 |
256 | scaled_rounded_x = [x_val * rect_size_x for x_val in rounded_x]
257 | scaled_rounded_y = [y_val * rect_size_y for y_val in rounded_y]
258 |
259 | # Filter out rows with None values
260 | filtered_data = [(x_val, y_val, z_val) for x_val, y_val, z_val in zip(scaled_rounded_x, scaled_rounded_y, binned_z) if None not in (x_val, y_val, z_val)]
261 |
262 | print(filtered_data)
263 | # Get the highest value of x and the lowest value of y from the binned data for origins
264 | x_origin = min(scaled_rounded_x)
265 | y_origin = HEIGHT - min(scaled_rounded_y) # using HEIGHT to flip the y-axis
266 |
267 | # For the endpoints:
268 | x_endpoint = max(scaled_rounded_x)
269 | y_endpoint = HEIGHT - max(scaled_rounded_y) # This will be the bottom of the screen
270 |
271 | # When drawing the X and Y axes:
272 | display.line(x_origin + x_offset, y_origin + y_offset, x_endpoint + x_offset, y_origin + y_offset, AXIS_THICKNESS)
273 | display.line(x_origin + x_offset, y_origin + y_offset, x_origin + x_offset, y_endpoint + y_offset, AXIS_THICKNESS)
274 |
275 | # Add ticks and labels for x-axis
276 | for i in range(0, int((x_endpoint - x_origin) / TICK_SPACING) + 1):
277 | if i % skip == 0:
278 | x_pos = x_origin + (i * TICK_SPACING)
279 | display.line(x_pos + x_offset, y_origin + y_offset + (AXIS_THICKNESS*2), x_pos + x_offset, y_origin - TICK_LENGTH + y_offset + (AXIS_THICKNESS*2), 1)
280 | display.text(str(i), x_pos + x_offset, y_origin + y_offset + TICK_LENGTH + 5, 1, 1) # Adjust the +5 for desired spacing
281 |
282 | y_axis_length = abs(y_origin - y_endpoint)
283 | num_ticks = y_axis_length // TICK_SPACING
284 |
285 | # Add ticks and labels for y-axis
286 | for i in range(num_ticks + 1):
287 | if i % skip == 0:
288 | y_tick_pos = y_origin - (i * TICK_SPACING)
289 | flipped_y = y_tick_pos + y_offset
290 | display.line(x_origin + x_offset, flipped_y, x_origin + x_offset - TICK_LENGTH, flipped_y, 1)
291 | display.text(str(i), x_origin + x_offset - TICK_LENGTH - 20, flipped_y, 1, 1) # Adjust the -20 for desired spacing
292 |
293 | # Loop through each filtered row of data
294 | for x_val, y_val, z_val in filtered_data:
295 | display.set_pen((15 - z_val))
296 | flipped_y = HEIGHT - y_val - rect_size_y
297 | display.rectangle(x_val + x_offset, flipped_y + y_offset, rect_size_x, rect_size_y)
298 | # Get the highest and lowest values of z for the legend
299 | max_z = max([z for (_, _, z) in filtered_data])
300 | min_z = min([z for (_, _, z) in filtered_data])
301 |
302 |
303 | # Define legend properties
304 | legend_steps = 5
305 | legend_width = 20
306 | legend_height = 10
307 |
308 | # Position the legend a bit to the right of the heatmap
309 | legend_x_start = x_endpoint + x_offset + 40
310 | legend_y_start = y_origin + y_offset - 10 # Position it just above the x-axis
311 |
312 | z_range = max(z) - min(z)
313 | z_step = z_range / z_bins_number
314 | z_thresholds = [min(z) + z_step * i for i in range(z_bins_number + 1)]
315 |
316 | # Draw the legend
317 | for step in range(legend_steps):
318 | z_val = step if step < len(z_thresholds) else z_bins_number - 1 # Use the binned values
319 | threshold_val = z_thresholds[step] if step < len(z_thresholds) else z_thresholds[-1] # Actual threshold value
320 |
321 | display_val = int(round(15 - z_val))
322 | display.set_pen(display_val)
323 |
324 | y_pos = legend_y_start - step * legend_height
325 | display.rectangle(legend_x_start, y_pos, legend_width, legend_height)
326 | display.set_pen(0)
327 | display.text(str(round(threshold_val, 2)), legend_x_start + legend_width + 5, y_pos, 1, 1)
328 |
329 | def plot_barchart(filename, x_name, AXIS_THICKNESS=3, TICK_LENGTH=5, x_offset=30, y_offset=-10, rect_size_x=4, rect_size_y=4, x_bins_number=50, skip=1):
330 | csv_data = read_csv(filename, x_name)
331 | x = csv_data[x_name]
332 |
333 | x_bins = bin_data(x, x_bins_number)
334 |
335 | counts = {}
336 | for x_bin in x_bins:
337 | if x_bin is not None:
338 | counts[x_bin] = counts.get(x_bin, 0) + 1
339 |
340 | filtered_data = [(x_bin * rect_size_x, count) for x_bin, count in counts.items()]
341 |
342 | x_origin = 0
343 | y_origin = HEIGHT
344 | x_endpoint = x_bins_number * rect_size_x
345 | max_count = max(counts.values())
346 | y_endpoint = HEIGHT - (max_count * rect_size_y)
347 |
348 | display.line(x_origin + x_offset, y_origin + y_offset, x_endpoint + x_offset, y_origin + y_offset, AXIS_THICKNESS)
349 | display.line(x_origin + x_offset, y_origin + y_offset, x_origin + x_offset, y_endpoint + y_offset, AXIS_THICKNESS)
350 |
351 | # X-axis ticks and labels
352 | for index, (x_val, _) in enumerate(filtered_data):
353 | display.line(x_val + x_offset, y_origin + y_offset + (AXIS_THICKNESS*2), x_val + x_offset, y_origin - TICK_LENGTH + y_offset + (AXIS_THICKNESS*2), 1)
354 |
355 | # Only print the label if the index is divisible by skip (starting from 0)
356 | if index % skip == 0:
357 | display.text(str(x_val//rect_size_x), x_val + x_offset, y_origin + y_offset + TICK_LENGTH + 5,1,1) # Adjust the +5 for desired spacing
358 |
359 | y_axis_length = abs(y_origin - y_endpoint)
360 |
361 | # Y-axis ticks and labels based on integer count values
362 | for i in range(0, max_count + 1):
363 | if i % skip == 0: # Only draw if the current index i is divisible by skip
364 | y_tick_pos = y_origin - (i * rect_size_y)
365 | flipped_y = y_tick_pos + y_offset
366 | display.line(x_origin + x_offset, flipped_y, x_origin + x_offset - TICK_LENGTH, flipped_y, 1)
367 | # Adding Y value as text label to the left of tick mark
368 | display.text(str(i), x_origin + x_offset - TICK_LENGTH - 20, flipped_y,1,1) # Adjust the -20 for desired spacing
369 |
370 | for x_val, count in filtered_data:
371 | print(f"x = {x_val}, count = {count}, xorigin = {x_val + x_offset}, yorigin = {y_origin + y_offset}, yend = {y_origin + y_offset - count *10}")
372 | display.set_pen(0)
373 | display.line(x_val + x_offset, y_origin + y_offset, x_val + x_offset, y_origin + y_offset - (count * rect_size_y), AXIS_THICKNESS)
374 | #display.update()
375 |
376 |
377 |
378 |
379 |
380 | clear()
381 |
382 | plot_barchart('data2.csv',
383 | x_name='x',
384 | AXIS_THICKNESS=3,
385 | TICK_LENGTH=5,
386 | x_offset=50,
387 | y_offset=-50,
388 | rect_size_x=4,
389 | rect_size_y=4,
390 | skip=5
391 | )
392 |
393 | display.update()
394 |
395 | clear()
396 |
397 | plot_heatmap_binned('data2.csv',
398 | x_name='x',
399 | y_name='y',
400 | z_name='z',
401 | AXIS_THICKNESS = 3,
402 | TICK_SPACING = 10,
403 | TICK_LENGTH = 5,
404 | x_offset = 20,
405 | y_offset = -10,
406 | rect_size_x = 9,
407 | rect_size_y = 9,
408 | x_bins_number = 10,
409 | y_bins_number = 10,
410 | z_bins_number = 10,
411 | skip=1)
412 |
413 | display.update()
414 |
415 | clear()
416 |
417 | plot_heatmap_rounded('data.csv',
418 | x_name='x',
419 | y_name='y',
420 | z_name='z',
421 | AXIS_THICKNESS=3,
422 | TICK_SPACING=10,
423 | TICK_LENGTH=5,
424 | x_offset=20,
425 | y_offset=-10,
426 | rect_size_x=9,
427 | rect_size_y=9,
428 | z_bins_number=10,
429 | skip=2)
430 | display.update()
431 |
432 |
--------------------------------------------------------------------------------
/Clock_Stuff/cl1.py:
--------------------------------------------------------------------------------
1 | import machine
2 | import badger2040
3 | import time
4 | import utime
5 | import ntptime
6 | from pcf85063a import PCF85063A
7 | import badger_os
8 |
9 | badger = badger2040.Badger2040()
10 |
11 | badger.set_pen(15)
12 | badger.clear()
13 | badger.set_pen(1)
14 |
15 | badger.connect()
16 |
17 | if badger.isconnected():
18 | # Synchronize with the NTP server to get the current time
19 | ntptime.settime()
20 |
21 | # Get the time after synchronizing with the NTP server
22 | ut = str(machine.RTC().datetime())
23 |
24 |
25 | badger.set_pen(15)
26 | badger.clear()
27 | badger.set_pen(1)
28 | badger.text(f"ut: {ut}", 10, 0, 1)
29 | badger.update()
30 | time.sleep(0.05)
31 |
32 | print(utime.localtime())
33 | # Set the time on the Pico's onboard RTC
34 | def set_pico_time():
35 | rtc = machine.RTC()
36 | now = utime.localtime()
37 | rtc.datetime((now[0], now[1], now[2], now[6], now[3], now[4], now[5], 0))
38 |
39 | # Set the time on the external PCF85063A RTC
40 | def set_pcf85063a_time():
41 | now = utime.localtime()
42 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
43 | rtc_pcf85063a = PCF85063A(i2c)
44 | rtc_pcf85063a.datetime((now[0], now[1], now[2], now[3], now[4], now[5], now[6]))
45 |
46 | # Set the time on the Pico's onboard RTC
47 | set_pico_time()
48 |
49 | # Set the time on the external PCF85063A RTC
50 | set_pcf85063a_time()
51 |
52 | # Get the time after setting the RTCs
53 | ut2 = str(machine.RTC().datetime())
54 |
55 |
56 | badger.text(f"Pico_RTC: {ut}", 80, 0, 1)
57 | badger.text(f"PCF_RTC: {ut2}", 200, 0, 1)
58 | badger.update()
59 | time.sleep(0.05)
60 |
61 | print("Pico RTC:", ut)
62 | print("PCF85063A RTC:", ut2)
63 |
64 | badger_os.launch("launcher")
65 |
66 |
--------------------------------------------------------------------------------
/Clock_Stuff/cl2.py:
--------------------------------------------------------------------------------
1 | import machine
2 | import badger2040
3 | import utime
4 | from pcf85063a import PCF85063A
5 |
6 | # Create Badger2040 instance
7 | display = badger2040.Badger2040()
8 |
9 | # Create Pico's RTC instance
10 | rtc = machine.RTC()
11 |
12 | # Create PCF85063A RTC instance
13 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
14 | rtc_pcf85063a = PCF85063A(i2c)
15 |
16 | # Clear screen
17 | display.set_pen(15)
18 | display.clear()
19 | display.set_pen(1)
20 |
21 | # Display system's time
22 | display.text(f"system_time: {utime.localtime()}", 10, 0, 1)
23 | display.update()
24 | utime.sleep(0.02)
25 |
26 | # Display Pico's RTC
27 | display.set_pen(15)
28 | display.clear()
29 | display.set_pen(1)
30 | display.text(f"pico_RTC: {rtc.datetime()}", 10, 0, 1)
31 | display.update()
32 | utime.sleep(0.02)
33 |
34 | # Display PCF85063A's RTC
35 | display.set_pen(15)
36 | display.clear()
37 | display.set_pen(1)
38 | display.text(f"PCF_RTC: {rtc_pcf85063a.datetime()}", 10, 0, 1)
39 | display.update()
40 | utime.sleep(0.02)
--------------------------------------------------------------------------------
/Clock_Stuff/icon-cl1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/Clock_Stuff/icon-cl1.jpg
--------------------------------------------------------------------------------
/Clock_Stuff/icon-cl2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/Clock_Stuff/icon-cl2.jpg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Chrissy h Roberts (He/Him)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Badger 2040W code resources
2 |
3 | This repo consolidates a bunch of code for the Pimoroni Badger2040 and Badger2040W
4 |
5 | NB : There's an issue with Mac version of Thonny which means it sometimes can't find the badger. Killing the internal library with `rm -rf ~/Library/Thonny` can fix this.
6 |
7 | ## App provisioning [examples/apps.py](examples/apps.py) and [examples/icon-apps.jpg](examples/icon-apps.jpg)
8 |
9 | Using Thonny to copy your code and apps on to the badger2040W can be a bit of a pain. This functionality allows a user to provision a list of apps to the device remotely. The main function here is that you can write an app and stick it in an 'examples' folder on a github repo or other source. The 'apps' app then consults a json file which maintains a list of the apps that you currently want on your badger2040W. It downloads the apps from your repo, then restarts the launcher to update the badgeros homepage.
10 |
11 | You'll need to make a new file `provisioning_manifest.json`.
12 |
13 | The contents are two lists `folders_to_clean` and `files`
14 |
15 | `folders_to_clean` tells the provisioning app to delete the contents of folders specified here. This has the effect of cleaning out things that are not on the list
16 | `files` provides [a] the path (on github) and filename of files you want to add and [b] the target folder on the badger 2040W.
17 |
18 | ```
19 | {
20 | "folders_to_clean": ["examples", "icons","data"],
21 | "files": [
22 | { "path": "examples/apps.py", "folder": "examples"},
23 | { "path": "examples/icon-apps.jpg", "folder": "examples"},
24 | { "path": "examples/weather.py", "folder": "examples"},
25 | { "path": "examples/icon-weather.jpg", "folder": "examples"},
26 | { "path": "examples/space.py", "folder": "examples"},
27 | { "path": "examples/icon-space.jpg", "folder": "examples"},
28 | { "path": "examples/power.py", "folder": "examples"},
29 | { "path": "examples/icon-power.jpg", "folder": "examples"},
30 | { "path": "data/data.csv", "folder": "data"},
31 | { "path": "data/data2.csv", "folder": "data"},
32 | { "path": "icons/icon-sun.jpg", "folder": "icons"},
33 | { "path": "icons/icon-snow.jpg", "folder": "icons"},
34 | { "path": "icons/icon-storm.jpg", "folder": "icons"},
35 | { "path": "icons/icon-rain.jpg", "folder": "icons"},
36 | { "path": "icons/icon-cloud.jpg", "folder": "icons"}
37 | ]
38 | }
39 | ```
40 |
41 | Ensure that you always have the `apps.py` and `icon-apps.jpg` on this list, or you'll immediately lose the provisioning functionality
42 |
43 | Don't forget to add an icon for each app in the `examples` folder, or the system will freeze.
44 |
45 | The first time you want to run the provisioning app, you'll need to manually install it with thonny.
46 | You'll also need the `WIFI_CONFIG.py` to be configured.
47 |
48 | Finally, you'll need to set the target for the repo.
49 |
50 | Change the line `github_repo_url = "https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/main/"`
51 | to match your own target. Put the `provisioning_manifest.json` file in the root of the repo.
52 |
53 |
54 | After the first install you won't _need_ Thonny anymore.
55 |
56 | 
57 | 
58 |
59 |
60 |
61 |
62 | ## [Charts](charts/heatmap.py)
63 |
64 | These scripts add some basic data visualisation methods to the badger. These can be used in projects that perform data logging across time, or any context where a dataset is pulled from an onboard or remote data source. There's limits on how big a table can be ingested, which probably simply relate to (a) the limited storage capacity and (b) the available RAM.
65 |
66 | 
67 | 
68 | 
69 |
70 | ## [Clock_Stuff](Clock_Stuff)
71 |
72 | contains a couple of scripts which explore how the RTC functions
73 |
74 | It’s been unclear to me which of the various ways to call the time are actually calling to the clock which stays active on battery power when unplugged from the USB cable.
75 |
76 | To call the time, I’ve typically used machine.RTC().datetime() and utime.localtime(), but neither of these seems to persist after the USB connection breaks or after the current script goes on to halt.
77 |
78 | A bit of google work identified that I may need to access the pcf85063a rtc directly on the RPI2040W chip.
79 | To explore this I made two separate scripts
80 |
81 | cl1.py connects to wifi and uses ntptime to synchronise the clocks on the badger2040w.
82 | It then calls machine.RTC().datetime(), utime.localtime() and the time on the pcf85063a rtc PCF85063A(i2c).datetime() sequentially. It displays the time on each clock on the screen of the badger.
83 |
84 | Starting with the badger2040W disconnected, I attach a battery and run the cl1.py script.
85 |
86 | 
87 |
88 | In this image you can see that all three clock interfaces are null, having been reset when the battery was disconnected.
89 |
90 | I then killed the cl1.py script and ran cl2.py
91 |
92 | The big difference here is that cl2.py doesn’t connect to ntptime. It just asks for the time from each of the three clock systems.
93 |
94 | Here’s what it returns
95 |
96 | 
97 |
98 | As you can see, the only method of the three which actually keeps the time inbetween two different scripts being run on the badger2040W is the pcf85063a method.
99 |
100 | My take home from this is that you should use the pcf85063a to establish any rtc link to the badger.
101 |
102 | The minimal code needed to pull time from the RTC is
103 |
104 | ```
105 | from pcf85063a import PCF85063A
106 | # Create PCF85063A RTC instance
107 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
108 | rtc_pcf85063a = PCF85063A(i2c)
109 | print(rtc_pcf85063a.datetime())
110 | ```
111 |
112 | I’ve tested that this method works both for a li-on 3.7 V battery plugged in to the batt socket on the back, and also for a badger running on a USB cable connected to a mobile phone charger pack. The RTC keeps running on both, even though I had to push the button on the charger pack to start pushing buttons. It seems that the buttons on the badger can’t trigger the activation of the generic mobile charger, but the RTC can keep running.
113 |
114 | ## Dashboard Calendar, TOTP and Clock [examples/dash.py](examples/dash.py)
115 |
116 | This pulls together some code from the clock and TOTP apps to build a simple dashboard. The main new feature here is the ability to read in a calendar .ics feed and to display current and next meetings, along with date, clock and current TOTP code. Refreshes every minute, so you might want to change to 30s if you want TOTPs showing correctly all the time. Pressing A will update TOTP, Pressing B updates both TOTP and Calendar events (it is consequentially slower to update) and button C puts the device to sleep or wakes it from sleep.
117 |
118 | There's built in colour switching between dark and light modes, which aims to minimise screen-burn, but my guess is that once you've used this dashboard for a while, you might well get ghosting thanks to the many refreshes you'll be doing.
119 |
120 | You'll need the URL to your ics calendar in `data/calendar_url.txt` and the TOTP magic code in `data/totp_keys.json` as per the totp app below.
121 |
122 | To do
123 | * Add weather
124 | * Add automatic on/off to map with working days
125 | * Prettify it
126 | * Handle issue where long event names wrap over multiple lines
127 | *
128 |
129 | 
130 |
131 |
132 | ## Space Weather [examples/space.py](examples/space.py) and [examples/icon-space.jpg](examples/icon-space.jpg)
133 |
134 | The space app adds functions to display a variety of data that can be useful to HAM radio / Amateur radio enthusiasts.
135 | The data that populate this app come from the excellent resources at at https://www.hamqsl.com/solarxml.php
136 |
137 | **NOTE: Please respect the request of the maintainers of www.hamqsl.com that you don't put too much strain on their resource. Downloading data more than once an hour is pointless and could harm their ability to continue to provide the data. If you value this, please donate to Paul L Herrman (N0NBH) who maintains it : [Donate here via PayPal](https://www.paypal.com/donate?token=PGsbxxaNxFueJmdq1fgPek22o4yU0UR6tybC7O1mUM66rCnWMDxZjvQtmFIAISSAwA2GZXfBMNPVzMTY)**
138 |
139 | **NOTE 2 : Because this project relies on the efforts and goodwill of the maintainers of www.hamqsl.com, the design of this app comes with a potential single point of failure (SPOF) risk. I don't know where the data really comes from and I'm sure that there's a more sustainable and lower risk route to getting this data through an API somewhere. If you know what the source is, then I'd appreciate it if you could share this info via the issues**
140 |
141 | In order for the local weather to be properly displayed, you should change the latitude and longitude in the script to match your location.
142 |
143 | 
144 |
145 | ## Temperature and Humidity Logger (AHT20 unit) [examples/weather.py](examples/logger.py) and [examples/icon-weather.jpg](examples/icon-logger.jpg) and [lib/ahtx0.py](lib/ahtx0.py)
146 |
147 | The [AHT20](https://learn.adafruit.com/adafruit-aht20/overview) connects to the QWIIC socket on the badger2040W and provides a rough temperature and humidity measurement on a schedule of your choice. It is controlled through the i2c protocol using a [library obtained here](https://raw.githubusercontent.com/targetblank/micropython_ahtx0/master/ahtx0.py) and copied to the [lib](lib) folder of this repo.
148 |
149 | The logger has basic functions to (a) collect, (b) visualise and (c) store to file, a set of measurements from the AHT20. This provides a framework for other types of environmental sensor connected via QWIIC.
150 |
151 | 
152 | 
153 |
154 |
155 | ## Terrestrial Weather [examples/weather.py](examples/weather.py) and [examples/icon-weather.jpg](examples/icon-weather.jpg)
156 |
157 | This is an updated version of the example weather app for the Badger2040W. I fiddled around with the calls to the open-meteo API, adding a bunch of new functions and data outputs. This now adds info about pollen levels, particulates in the air, rainfall level and probability, UV index, winds, sunrise and sunset. It also adds a 2 day forecast.
158 |
159 | 
160 |
161 | ## TOTP Authenticator [examples/totp.py](examples/totp.py) and [examples/icon-totp.jpg](examples/icon-totp.jpg)
162 |
163 | This app provides the functionality of a TOTP authenticator. It is tested against Google Authenticator and creates identical codes, on the same time step.
164 | When launched, the app connects to the web and synchronises the system clock via `ntptime` server. Currently unclear if it automatically handles timezones. I think so but will check after October 31^st^
165 | To add you keys, you need to get the secret keys from your service provider and add them to the [data/totp_keys.json](data/totp_keys.json) file.
166 | You should be able to add 30 keys to the same screen. Best to have this plugged in, as battery will drain faster with 30s updates.
167 |
168 | 
169 |
170 | A lot of the code comes from this project by Edd Mann [https://github.com/eddmann/pico-2fa-totp](https://github.com/eddmann/pico-2fa-totp)
171 |
172 | ## TOTP Authenticator 2 [examples/totp2.py](examples/totp2.py) and [examples/icon-totp2.jpg](examples/icon-totp2.jpg)
173 |
174 | The exact same thing as totp, except it only updates the screen when you press a button.
175 |
176 | ## [3D Printable Badger2040 / Badger2040W case](3d_print_case)
177 |
178 | This is an openscad model and STL file for a really simple backplate for the badger2040W. You can screw your badger on to this with some small screws. It has a space for the USB socket and also ample room in the back for a li-on battery pack. I used a 1200 mAh PKCELL from Pimoroni.
179 |
180 | 
181 | 
182 |
183 |
184 | ## Support this project
185 |
186 | If you would like to support this project, please feel free to pay what you want https://t.co/GpUNwewruR
187 |
--------------------------------------------------------------------------------
/Wifi_Toggle.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | import network_manager
3 | import WIFI_CONFIG
4 | import utime
5 | from machine import Pin
6 | import urequests
7 |
8 | # Initialize the Badger2040
9 | badger = badger2040.Badger2040()
10 | network_manager_instance = network_manager.NetworkManager(country="GB")
11 |
12 | def toggle_wifi(state):
13 | wlan = network_manager_instance._sta_if
14 | if state == "on":
15 | # Activate Wi-Fi
16 | wlan.active(True)
17 | wlan.connect(WIFI_CONFIG.SSID, WIFI_CONFIG.PSK)
18 | utime.sleep(5) # Wait for connection
19 | if wlan.isconnected():
20 | print(f"Wi-Fi connected with IP address: {wlan.ifconfig()[0]}")
21 | else:
22 | print("Wi-Fi connection failed.")
23 | elif state == "off":
24 | # Deactivate Wi-Fi
25 | network_manager_instance._sta_if.disconnect()
26 | print("Wi-Fi disconnected from the network.")
27 | print(f"Wi-Fi with IP address: {wlan.ifconfig()[0]}")
28 |
29 | else:
30 | print("Invalid state. Use 'on' or 'off'.")
31 |
32 |
33 | # Example usage
34 | toggle_wifi("on")
35 |
36 |
37 | # Fetch a chunk of data after connecting
38 | if network_manager_instance._sta_if.isconnected():
39 | URL = "http://httpbin.org/bytes/1024" # Fetch 1KB of data
40 | try:
41 | response = urequests.get(URL)
42 | if response.status_code == 200:
43 | data_chunk = response.content
44 | print(f"Received data chunk: {data_chunk[:100]}...") # Print first 100 bytes as a preview
45 | else:
46 | print(f"Failed to fetch data. Status code: {response.status_code}")
47 | response.close()
48 | except Exception as e:
49 | print(f"An error occurred: {e}")
50 | else:
51 | print("Wi-Fi not connected. Unable to fetch data.")
52 |
53 | # Turn off Wi-Fi after use
54 | toggle_wifi("off")
55 |
56 |
57 |
--------------------------------------------------------------------------------
/ahtx0.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 | #
3 | # Copyright (c) 2020 Kattni Rembor for Adafruit Industries
4 | # Copyright (c) 2020 Andreas Bühl
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | """
24 |
25 | MicroPython driver for the AHT10 and AHT20 Humidity and Temperature Sensor
26 |
27 | Author(s): Andreas Bühl, Kattni Rembor
28 |
29 | """
30 |
31 | import utime
32 | from micropython import const
33 |
34 |
35 | class AHT10:
36 | """Interface library for AHT10/AHT20 temperature+humidity sensors"""
37 |
38 | AHTX0_I2CADDR_DEFAULT = const(0x38) # Default I2C address
39 | AHTX0_CMD_INITIALIZE = 0xE1 # Initialization command
40 | AHTX0_CMD_TRIGGER = const(0xAC) # Trigger reading command
41 | AHTX0_CMD_SOFTRESET = const(0xBA) # Soft reset command
42 | AHTX0_STATUS_BUSY = const(0x80) # Status bit for busy
43 | AHTX0_STATUS_CALIBRATED = const(0x08) # Status bit for calibrated
44 |
45 | def __init__(self, i2c, address=AHTX0_I2CADDR_DEFAULT):
46 | utime.sleep_ms(20) # 20ms delay to wake up
47 | self._i2c = i2c
48 | self._address = address
49 | self._buf = bytearray(6)
50 | self.reset()
51 | if not self.initialize():
52 | raise RuntimeError("Could not initialize")
53 | self._temp = None
54 | self._humidity = None
55 |
56 | def reset(self):
57 | """Perform a soft-reset of the AHT"""
58 | self._buf[0] = self.AHTX0_CMD_SOFTRESET
59 | self._i2c.writeto(self._address, self._buf[0:1])
60 | utime.sleep_ms(20) # 20ms delay to wake up
61 |
62 | def initialize(self):
63 | """Ask the sensor to self-initialize. Returns True on success, False otherwise"""
64 | self._buf[0] = self.AHTX0_CMD_INITIALIZE
65 | self._buf[1] = 0x08
66 | self._buf[2] = 0x00
67 | self._i2c.writeto(self._address, self._buf[0:3])
68 | self._wait_for_idle()
69 | if not self.status & self.AHTX0_STATUS_CALIBRATED:
70 | return False
71 | return True
72 |
73 | @property
74 | def status(self):
75 | """The status byte initially returned from the sensor, see datasheet for details"""
76 | self._read_to_buffer()
77 | return self._buf[0]
78 |
79 | @property
80 | def relative_humidity(self):
81 | """The measured relative humidity in percent."""
82 | self._perform_measurement()
83 | self._humidity = (
84 | (self._buf[1] << 12) | (self._buf[2] << 4) | (self._buf[3] >> 4)
85 | )
86 | self._humidity = (self._humidity * 100) / 0x100000
87 | return self._humidity
88 |
89 | @property
90 | def temperature(self):
91 | """The measured temperature in degrees Celcius."""
92 | self._perform_measurement()
93 | self._temp = ((self._buf[3] & 0xF) << 16) | (self._buf[4] << 8) | self._buf[5]
94 | self._temp = ((self._temp * 200.0) / 0x100000) - 50
95 | return self._temp
96 |
97 | def _read_to_buffer(self):
98 | """Read sensor data to buffer"""
99 | self._i2c.readfrom_into(self._address, self._buf)
100 |
101 | def _trigger_measurement(self):
102 | """Internal function for triggering the AHT to read temp/humidity"""
103 | self._buf[0] = self.AHTX0_CMD_TRIGGER
104 | self._buf[1] = 0x33
105 | self._buf[2] = 0x00
106 | self._i2c.writeto(self._address, self._buf[0:3])
107 |
108 | def _wait_for_idle(self):
109 | """Wait until sensor can receive a new command"""
110 | while self.status & self.AHTX0_STATUS_BUSY:
111 | utime.sleep_ms(5)
112 |
113 | def _perform_measurement(self):
114 | """Trigger measurement and write result to buffer"""
115 | self._trigger_measurement()
116 | self._wait_for_idle()
117 | self._read_to_buffer()
118 |
119 |
120 | class AHT20(AHT10):
121 | AHTX0_CMD_INITIALIZE = 0xBE # Calibration command
122 |
--------------------------------------------------------------------------------
/apps_provisioning/apps.py:
--------------------------------------------------------------------------------
1 | import urequests as requests
2 | import ujson as json
3 | import machine
4 | import badger2040
5 | import os
6 |
7 | from badger2040 import WIDTH
8 |
9 | display = badger2040.Badger2040()
10 | display.set_update_speed(2)
11 | display.connect()
12 |
13 | ##########################################################################################
14 | # Define a function that clears the screen and prints a header row
15 | ##########################################################################################
16 | def clear():
17 | display.set_pen(15)
18 | display.clear()
19 | display.set_pen(0)
20 | display.set_font("bitmap8")
21 | display.set_pen(0)
22 | display.rectangle(0, 0, WIDTH, 10)
23 | display.set_pen(15)
24 | display.text("Badger App provisioning", 10, 1, WIDTH, 0.6) # parameters are left padding, top padding, width of screen area, font size
25 | display.set_pen(0)
26 |
27 | def download_file(url, destination_path):
28 | print(f"Downloading {url} to {destination_path}")
29 | response = requests.get(url)
30 | if response.status_code == 200:
31 | with open(destination_path, "wb") as f:
32 | f.write(response.content)
33 | print("Download complete")
34 | else:
35 | print("Failed to download:", url)
36 |
37 | github_repo_url = "https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/main/"
38 | provisioning_manifest_url = github_repo_url + "provisioning_manifest.json"
39 |
40 | print(f"provisioning manifest URL is : {provisioning_manifest_url}")
41 |
42 | # Retrieve provisioning manifest
43 | manifest_response = requests.get(provisioning_manifest_url)
44 |
45 | if manifest_response.status_code == 200:
46 | print("Provisioning manifest downloaded successfully.")
47 | clear()
48 | display.text("Provisioning manifest downloaded successfully.", 10, 35, WIDTH, 1)
49 | display.update()
50 | manifest_data = json.loads(manifest_response.content)
51 | folders_to_clean = manifest_data.get("folders_to_clean", [])
52 | files_to_keep = manifest_data["files"]
53 |
54 | # Clean up folders specified in the manifest
55 | for folder in folders_to_clean:
56 | print(f"Cleaning up folder: {folder}")
57 | folder_path = "./" + folder + "/"
58 | try:
59 | for filename in os.listdir(folder_path):
60 | entry_path = folder_path + filename
61 | # Check if it's a regular file (not a directory)
62 | try:
63 | with open(entry_path, "rb"):
64 | os.remove(entry_path)
65 | print("Removed:", filename)
66 | except OSError:
67 | pass
68 | except OSError:
69 | pass
70 |
71 | # Download and add files from manifest
72 | print("Downloading files from manifest...")
73 | clear()
74 | display.text(f"Downloading files in manifest...", 10, 15, WIDTH, 1)
75 |
76 | num_files = len(files_to_keep)
77 | for index, file_info in enumerate(files_to_keep, start=1):
78 | file_path = file_info["path"]
79 | file_folder = file_info.get("folder", "examples")
80 | print(f"File {file_path} ({index}/{num_files})")
81 | file_url = github_repo_url + file_path
82 | print(f"Downloading: {file_url}")
83 | destination_folder = "./" + file_folder
84 | try:
85 | os.mkdir(destination_folder)
86 | except OSError:
87 | pass
88 | destination_path = destination_folder + "/" + file_path.split("/")[-1]
89 | print(f"Saving to: {destination_path}")
90 | download_file(file_url, destination_path)
91 | print("Downloaded:", file_path)
92 |
93 | # Calculate the vertical position dynamically based on index
94 | text_vertical_position = 25 + (10 * ((index - 1) % 10))
95 |
96 | display.text(f"Downloaded: {file_path} ({index}/{num_files})", 10, text_vertical_position, WIDTH, 1)
97 | display.update()
98 |
99 | # Check if it's the 10th download, then clear display and show progress
100 | if index % 10 == 0:
101 | clear()
102 | display.text(f"Downloading files in manifest...", 10, 15, WIDTH, 1)
103 | display.update()
104 |
105 | clear()
106 | display.text(f"Provisioning complete", 10, 15, WIDTH, 1)
107 | display.update()
108 |
--------------------------------------------------------------------------------
/apps_provisioning/icon-apps.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/apps_provisioning/icon-apps.jpg
--------------------------------------------------------------------------------
/data/totp_keys.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "TEST ACCOUNT",
4 | "key": "AC56YGHG78TJSNYO5R6OAM6HWQO6XZ5L"
5 | },
6 | {
7 | "name": "BADGER TOTP",
8 | "key": "LMERQHIC2MNHKLYO5R6OAM6HWQO6XZ5L"
9 | }
10 | ]
--------------------------------------------------------------------------------
/examples/apps.py:
--------------------------------------------------------------------------------
1 | import urequests as requests
2 | import ujson as json
3 | import machine
4 | import badger2040
5 | import os
6 |
7 | from badger2040 import WIDTH
8 |
9 | display = badger2040.Badger2040()
10 | display.set_update_speed(2)
11 | display.connect()
12 |
13 | ##########################################################################################
14 | # USER DEFINED VARIABLES
15 | # Set the github repo
16 | # Note that you need to specify the raw.githubusercontent.com/ version of the URL
17 | ##########################################################################################
18 |
19 | github_repo_url = "https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/main/"
20 |
21 | ##########################################################################################
22 | ##########################################################################################
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ##########################################################################################
31 | ##########################################################################################
32 | # FUNCTIONS
33 | ##########################################################################################
34 | ##########################################################################################
35 |
36 |
37 |
38 | ##########################################################################################
39 | # Define a function that clears the screen and prints a header row
40 | ##########################################################################################
41 | def clear():
42 | display.set_pen(15)
43 | display.clear()
44 | display.set_pen(0)
45 | display.set_font("bitmap8")
46 | display.set_pen(0)
47 | display.rectangle(0, 0, WIDTH, 10)
48 | display.set_pen(15)
49 | display.text("Badger App provisioning", 10, 1, WIDTH, 0.6) # parameters are left padding, top padding, width of screen area, font size
50 | display.set_pen(0)
51 |
52 | ##########################################################################################
53 | # Define a function that downloads a file from the manifest
54 | ##########################################################################################
55 |
56 | def download_file(url, destination_path):
57 | print(f"Downloading {url} to {destination_path}")
58 | response = requests.get(url)
59 | if response.status_code == 200:
60 | with open(destination_path, "wb") as f:
61 | f.write(response.content)
62 | print("Download complete")
63 | else:
64 | print("Failed to download:", url)
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | ##########################################################################################
73 | ##########################################################################################
74 | # MAIN
75 | ##########################################################################################
76 | ##########################################################################################
77 |
78 | provisioning_manifest_url = github_repo_url + "provisioning_manifest.json"
79 | print(f"provisioning manifest URL is : {provisioning_manifest_url}")
80 |
81 | ##########################################################################################
82 | # Retrieve provisioning manifest
83 | ##########################################################################################
84 | manifest_response = requests.get(provisioning_manifest_url)
85 |
86 | if manifest_response.status_code == 200:
87 | print("Provisioning manifest downloaded successfully.")
88 | clear()
89 | display.text("Provisioning manifest downloaded successfully.", 10, 35, WIDTH, 1)
90 | display.update()
91 | manifest_data = json.loads(manifest_response.content)
92 | folders_to_clean = manifest_data.get("folders_to_clean", [])
93 | files_to_keep = manifest_data["files"]
94 |
95 | ##########################################################################################
96 | # Clean up folders specified in the manifest - i.e. delete contents of these folders
97 | ##########################################################################################
98 | for folder in folders_to_clean:
99 | print(f"Cleaning up folder: {folder}")
100 | folder_path = "./" + folder + "/"
101 | try:
102 | for filename in os.listdir(folder_path):
103 | entry_path = folder_path + filename
104 | # Check if it's a regular file (not a directory)
105 | try:
106 | with open(entry_path, "rb"):
107 | os.remove(entry_path)
108 | print("Removed:", filename)
109 | except OSError:
110 | pass
111 | except OSError:
112 | pass
113 | ##########################################################################################
114 | # Download and add files from manifest to the appropriate folders
115 | ##########################################################################################
116 | print("Downloading files from manifest...")
117 | clear()
118 | display.text(f"Downloading files in manifest...", 10, 15, WIDTH, 1)
119 |
120 | num_files = len(files_to_keep)
121 | for index, file_info in enumerate(files_to_keep, start=1):
122 | file_path = file_info["path"]
123 | file_folder = file_info.get("folder", "examples")
124 | print(f"File {file_path} ({index}/{num_files})")
125 | file_url = github_repo_url + file_path
126 | print(f"Downloading: {file_url}")
127 | destination_folder = "./" + file_folder
128 | try:
129 | os.mkdir(destination_folder)
130 | except OSError:
131 | pass
132 | destination_path = destination_folder + "/" + file_path.split("/")[-1]
133 | print(f"Saving to: {destination_path}")
134 | download_file(file_url, destination_path)
135 | print("Downloaded:", file_path)
136 |
137 | # Calculate the vertical position dynamically based on index
138 | text_vertical_position = 25 + (10 * ((index - 1) % 10))
139 |
140 | display.text(f"Downloaded: {file_path} ({index}/{num_files})", 10, text_vertical_position, WIDTH, 1)
141 | display.update()
142 |
143 | # Check if it's the 10th download, then clear display and show progress
144 | if index % 10 == 0:
145 | clear()
146 | display.text(f"Downloading files in manifest...", 10, 15, WIDTH, 1)
147 | display.update()
148 |
149 | ##########################################################################################
150 | clear()
151 | display.text(f"Provisioning complete", 10, 15, WIDTH, 1)
152 | display.text(f"Press a + c to exit to badger OS", 10, 25, WIDTH, 1)
153 | display.update()
154 |
155 | while True:
156 | display.keepalive()
157 | display.halt()
158 |
159 | ##########################################################################################
160 | ##########################################################################################
161 | # END
162 | ##########################################################################################
163 | ##########################################################################################
164 |
165 |
--------------------------------------------------------------------------------
/examples/badge.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | import jpegdec
3 |
4 |
5 | # Global Constants
6 | WIDTH = badger2040.WIDTH
7 | HEIGHT = badger2040.HEIGHT
8 |
9 | IMAGE_WIDTH = 104
10 |
11 | COMPANY_HEIGHT = 30
12 | DETAILS_HEIGHT = 20
13 | NAME_HEIGHT = HEIGHT - COMPANY_HEIGHT - (DETAILS_HEIGHT * 2) - 2
14 | TEXT_WIDTH = WIDTH - IMAGE_WIDTH - 1
15 |
16 | COMPANY_TEXT_SIZE = 0.6
17 | DETAILS_TEXT_SIZE = 0.5
18 |
19 | LEFT_PADDING = 5
20 | NAME_PADDING = 20
21 | DETAIL_SPACING = 10
22 |
23 | BADGE_PATH = "/badges/badge.txt"
24 |
25 | DEFAULT_TEXT = """mustelid inc
26 | H. Badger
27 | RP2040
28 | 2MB Flash
29 | E ink
30 | 296x128px
31 | /badges/badge.jpg
32 | """
33 |
34 | # ------------------------------
35 | # Utility functions
36 | # ------------------------------
37 |
38 |
39 | # Reduce the size of a string until it fits within a given width
40 | def truncatestring(text, text_size, width):
41 | while True:
42 | length = display.measure_text(text, text_size)
43 | if length > 0 and length > width:
44 | text = text[:-1]
45 | else:
46 | text += ""
47 | return text
48 |
49 |
50 | # ------------------------------
51 | # Drawing functions
52 | # ------------------------------
53 |
54 | # Draw the badge, including user text
55 | def draw_badge():
56 | display.set_pen(0)
57 | display.clear()
58 |
59 | # Draw badge image
60 | jpeg.open_file(badge_image)
61 | jpeg.decode(WIDTH - IMAGE_WIDTH, 0)
62 |
63 | # Draw a border around the image
64 | display.set_pen(0)
65 | display.line(WIDTH - IMAGE_WIDTH, 0, WIDTH - 1, 0)
66 | display.line(WIDTH - IMAGE_WIDTH, 0, WIDTH - IMAGE_WIDTH, HEIGHT - 1)
67 | display.line(WIDTH - IMAGE_WIDTH, HEIGHT - 1, WIDTH - 1, HEIGHT - 1)
68 | display.line(WIDTH - 1, 0, WIDTH - 1, HEIGHT - 1)
69 |
70 | # Uncomment this if a white background is wanted behind the company
71 | # display.set_pen(15)
72 | # display.rectangle(1, 1, TEXT_WIDTH, COMPANY_HEIGHT - 1)
73 |
74 | # Draw the company
75 | display.set_pen(15) # Change this to 0 if a white background is used
76 | display.set_font("serif")
77 | display.text(company, LEFT_PADDING, (COMPANY_HEIGHT // 2) + 1, WIDTH, COMPANY_TEXT_SIZE)
78 |
79 | # Draw a white background behind the name
80 | display.set_pen(15)
81 | display.rectangle(1, COMPANY_HEIGHT + 1, TEXT_WIDTH, NAME_HEIGHT)
82 |
83 | # Draw the name, scaling it based on the available width
84 | display.set_pen(0)
85 | display.set_font("sans")
86 | name_size = 2.0 # A sensible starting scale
87 | while True:
88 | name_length = display.measure_text(name, name_size)
89 | if name_length >= (TEXT_WIDTH - NAME_PADDING) and name_size >= 0.1:
90 | name_size -= 0.01
91 | else:
92 | display.text(name, (TEXT_WIDTH - name_length) // 2, (NAME_HEIGHT // 2) + COMPANY_HEIGHT + 1, WIDTH, name_size)
93 | break
94 |
95 | # Draw a white backgrounds behind the details
96 | display.set_pen(15)
97 | display.rectangle(1, HEIGHT - DETAILS_HEIGHT * 2, TEXT_WIDTH, DETAILS_HEIGHT - 1)
98 | display.rectangle(1, HEIGHT - DETAILS_HEIGHT, TEXT_WIDTH, DETAILS_HEIGHT - 1)
99 |
100 | # Draw the first detail's title and text
101 | display.set_pen(0)
102 | display.set_font("sans")
103 | name_length = display.measure_text(detail1_title, DETAILS_TEXT_SIZE)
104 | display.text(detail1_title, LEFT_PADDING, HEIGHT - ((DETAILS_HEIGHT * 3) // 2), WIDTH, DETAILS_TEXT_SIZE)
105 | display.text(detail1_text, 5 + name_length + DETAIL_SPACING, HEIGHT - ((DETAILS_HEIGHT * 3) // 2), WIDTH, DETAILS_TEXT_SIZE)
106 |
107 | # Draw the second detail's title and text
108 | name_length = display.measure_text(detail2_title, DETAILS_TEXT_SIZE)
109 | display.text(detail2_title, LEFT_PADDING, HEIGHT - (DETAILS_HEIGHT // 2), WIDTH, DETAILS_TEXT_SIZE)
110 | display.text(detail2_text, LEFT_PADDING + name_length + DETAIL_SPACING, HEIGHT - (DETAILS_HEIGHT // 2), WIDTH, DETAILS_TEXT_SIZE)
111 |
112 | display.update()
113 |
114 |
115 | # ------------------------------
116 | # Program setup
117 | # ------------------------------
118 |
119 | # Create a new Badger and set it to update NORMAL
120 | display = badger2040.Badger2040()
121 | display.led(128)
122 | display.set_update_speed(badger2040.UPDATE_NORMAL)
123 | display.set_thickness(2)
124 |
125 | jpeg = jpegdec.JPEG(display.display)
126 |
127 | # Open the badge file
128 | try:
129 | badge = open(BADGE_PATH, "r")
130 | except OSError:
131 | with open(BADGE_PATH, "w") as f:
132 | f.write(DEFAULT_TEXT)
133 | f.flush()
134 | badge = open(BADGE_PATH, "r")
135 |
136 | # Read in the next 6 lines
137 | company = badge.readline() # "mustelid inc"
138 | name = badge.readline() # "H. Badger"
139 | detail1_title = badge.readline() # "RP2040"
140 | detail1_text = badge.readline() # "2MB Flash"
141 | detail2_title = badge.readline() # "E ink"
142 | detail2_text = badge.readline() # "296x128px"
143 | badge_image = badge.readline() # /badges/badge.jpg
144 |
145 | # Truncate all of the text (except for the name as that is scaled)
146 | company = truncatestring(company, COMPANY_TEXT_SIZE, TEXT_WIDTH)
147 |
148 | detail1_title = truncatestring(detail1_title, DETAILS_TEXT_SIZE, TEXT_WIDTH)
149 | detail1_text = truncatestring(detail1_text, DETAILS_TEXT_SIZE,
150 | TEXT_WIDTH - DETAIL_SPACING - display.measure_text(detail1_title, DETAILS_TEXT_SIZE))
151 |
152 | detail2_title = truncatestring(detail2_title, DETAILS_TEXT_SIZE, TEXT_WIDTH)
153 | detail2_text = truncatestring(detail2_text, DETAILS_TEXT_SIZE,
154 | TEXT_WIDTH - DETAIL_SPACING - display.measure_text(detail2_title, DETAILS_TEXT_SIZE))
155 |
156 |
157 | # ------------------------------
158 | # Main program
159 | # ------------------------------
160 |
161 | draw_badge()
162 |
163 | while True:
164 | # Sometimes a button press or hold will keep the system
165 | # powered *through* HALT, so latch the power back on.
166 | display.keepalive()
167 |
168 | # If on battery, halt the Badger to save power, it will wake up if any of the front buttons are pressed
169 | display.halt()
170 |
--------------------------------------------------------------------------------
/examples/cl1.py:
--------------------------------------------------------------------------------
1 | import machine
2 | import badger2040
3 | import time
4 | import utime
5 | import ntptime
6 | from pcf85063a import PCF85063A
7 | import badger_os
8 |
9 | badger = badger2040.Badger2040()
10 |
11 | badger.set_pen(15)
12 | badger.clear()
13 | badger.set_pen(1)
14 |
15 | badger.connect()
16 |
17 | if badger.isconnected():
18 | # Synchronize with the NTP server to get the current time
19 | ntptime.settime()
20 |
21 |
22 | badger.set_pen(15)
23 | badger.clear()
24 | badger.set_pen(1)
25 | badger.update()
26 | time.sleep(0.05)
27 |
28 | # Set the time on the Pico's onboard RTC
29 | def set_pico_time():
30 | rtc = machine.RTC()
31 | now = utime.localtime()
32 | rtc.datetime((now[0], now[1], now[2], now[6], now[3], now[4], now[5], 0))
33 |
34 | # Set the time on the external PCF85063A RTC
35 | def set_pcf85063a_time():
36 | now = utime.localtime()
37 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
38 | rtc_pcf85063a = PCF85063A(i2c)
39 | rtc_pcf85063a.datetime(now)
40 |
41 | # Set the time on the Pico's onboard RTC
42 | set_pico_time()
43 |
44 | # Set the time on the external PCF85063A RTC
45 | set_pcf85063a_time()
46 |
47 | # Get the time after setting the RTCs
48 |
49 | badger.text(f"Pico_RTC: {ut}", 80, 0, 1)
50 | badger.text(f"PCF_RTC: {ut2}", 200, 0, 1)
51 | badger.update()
52 | time.sleep(0.05)
53 |
54 | print("Pico RTC:", utime.localtime())
55 | print("PCF85063A RTC:", str(machine.RTC().datetime()))
56 | while True:
57 | badger.keepalive()
58 | badger.halt()
59 |
60 |
--------------------------------------------------------------------------------
/examples/cl2.py:
--------------------------------------------------------------------------------
1 | import machine
2 | import badger2040
3 | import utime
4 | from pcf85063a import PCF85063A
5 |
6 | # Create Badger2040 instance
7 | display = badger2040.Badger2040()
8 |
9 | # Create Pico's RTC instance
10 | rtc = machine.RTC()
11 |
12 | # Create PCF85063A RTC instance
13 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
14 | rtc_pcf85063a = PCF85063A(i2c)
15 |
16 | # Clear screen
17 | display.set_pen(15)
18 | display.clear()
19 | display.set_pen(1)
20 |
21 | # Display system's time
22 | display.text(f"system_time: {utime.localtime()}", 10, 0, 1)
23 | display.update()
24 | utime.sleep(0.02)
25 |
26 | # Display Pico's RTC
27 | display.set_pen(15)
28 | display.clear()
29 | display.set_pen(1)
30 | display.text(f"pico_RTC: {rtc.datetime()}", 10, 0, 1)
31 | display.update()
32 | utime.sleep(0.02)
33 |
34 | # Display PCF85063A's RTC
35 | display.set_pen(15)
36 | display.clear()
37 | display.set_pen(1)
38 | display.text(f"PCF_RTC: {rtc_pcf85063a.datetime()}", 10, 0, 1)
39 | display.update()
40 | utime.sleep(0.02)
41 |
42 | while True:
43 | display.keepalive()
44 | display.halt()
--------------------------------------------------------------------------------
/examples/clock.py:
--------------------------------------------------------------------------------
1 | import time
2 | import machine
3 | import badger2040
4 |
5 |
6 | display = badger2040.Badger2040()
7 | display.set_update_speed(2)
8 | display.set_thickness(4)
9 |
10 | WIDTH, HEIGHT = display.get_bounds()
11 |
12 | if badger2040.is_wireless():
13 | import ntptime
14 | try:
15 | display.connect()
16 | if display.isconnected():
17 | ntptime.settime()
18 | except (RuntimeError, OSError) as e:
19 | print(f"Wireless Error: {e.value}")
20 |
21 | # Thonny overwrites the Pico RTC so re-sync from the physical RTC if we can
22 | try:
23 | badger2040.pcf_to_pico_rtc()
24 | except RuntimeError:
25 | pass
26 |
27 | rtc = machine.RTC()
28 |
29 | display.set_font("gothic")
30 |
31 | cursors = ["year", "month", "day", "hour", "minute"]
32 | set_clock = False
33 | toggle_set_clock = False
34 | cursor = 0
35 | last = 0
36 |
37 | button_a = badger2040.BUTTONS[badger2040.BUTTON_A]
38 | button_b = badger2040.BUTTONS[badger2040.BUTTON_B]
39 | button_c = badger2040.BUTTONS[badger2040.BUTTON_C]
40 |
41 | button_up = badger2040.BUTTONS[badger2040.BUTTON_UP]
42 | button_down = badger2040.BUTTONS[badger2040.BUTTON_DOWN]
43 |
44 |
45 | # Button handling function
46 | def button(pin):
47 | global last, set_clock, toggle_set_clock, cursor, year, month, day, hour, minute
48 |
49 | time.sleep(0.01)
50 | if not pin.value():
51 | return
52 |
53 | if button_a.value() and button_c.value():
54 | machine.reset()
55 |
56 | adjust = 0
57 |
58 | if pin == button_b:
59 | toggle_set_clock = True
60 | if set_clock:
61 | rtc.datetime((year, month, day, 0, hour, minute, second, 0))
62 | if badger2040.is_wireless():
63 | badger2040.pico_rtc_to_pcf()
64 | return
65 |
66 | if set_clock:
67 | if pin == button_c:
68 | cursor += 1
69 | cursor %= len(cursors)
70 |
71 | if pin == button_a:
72 | cursor -= 1
73 | cursor %= len(cursors)
74 |
75 | if pin == button_up:
76 | adjust = 1
77 |
78 | if pin == button_down:
79 | adjust = -1
80 |
81 | if cursors[cursor] == "year":
82 | year += adjust
83 | year = max(year, 2022)
84 | day = min(day, days_in_month(month, year))
85 |
86 | if cursors[cursor] == "month":
87 | month += adjust
88 | month = min(max(month, 1), 12)
89 | day = min(day, days_in_month(month, year))
90 |
91 | if cursors[cursor] == "day":
92 | day += adjust
93 | day = min(max(day, 1), days_in_month(month, year))
94 |
95 | if cursors[cursor] == "hour":
96 | hour += adjust
97 | hour %= 24
98 |
99 | if cursors[cursor] == "minute":
100 | minute += adjust
101 | minute %= 60
102 |
103 | draw_clock()
104 |
105 |
106 | def days_in_month(month, year):
107 | if month == 2 and ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0):
108 | return 29
109 | return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1]
110 |
111 |
112 | def draw_clock():
113 | global second_offset, second_unit_offset
114 |
115 | hms = "{:02}:{:02}:{:02}".format(hour, minute, second)
116 | ymd = "{:04}/{:02}/{:02}".format(year, month, day)
117 |
118 | hms_width = display.measure_text(hms, 1.8)
119 | hms_offset = int((badger2040.WIDTH / 2) - (hms_width / 2))
120 | h_width = display.measure_text(hms[0:2], 1.8)
121 | mi_width = display.measure_text(hms[3:5], 1.8)
122 | mi_offset = display.measure_text(hms[0:3], 1.8)
123 |
124 | ymd_width = display.measure_text(ymd, 1.0)
125 | ymd_offset = int((badger2040.WIDTH / 2) - (ymd_width / 2))
126 | y_width = display.measure_text(ymd[0:4], 1.0)
127 | m_width = display.measure_text(ymd[5:7], 1.0)
128 | m_offset = display.measure_text(ymd[0:5], 1.0)
129 | d_width = display.measure_text(ymd[8:10], 1.0)
130 | d_offset = display.measure_text(ymd[0:8], 1.0)
131 |
132 | display.set_pen(15)
133 | display.clear()
134 | display.set_pen(0)
135 | display.set_font("serif")
136 | display.set_thickness(2)
137 | display.text(hms, hms_offset, 40, 0, 1.8)
138 | display.text(ymd, ymd_offset, 100, 0, 1.0)
139 |
140 | hms = "{:02}:{:02}:".format(hour, minute)
141 | second_offset = hms_offset + display.measure_text(hms, 1.8)
142 | hms = "{:02}:{:02}:{}".format(hour, minute, second // 10)
143 | second_unit_offset = hms_offset + display.measure_text(hms, 1.8)
144 |
145 | if set_clock:
146 | display.set_pen(0)
147 | if cursors[cursor] == "year":
148 | display.line(ymd_offset, 120, ymd_offset + y_width, 120, 4)
149 | if cursors[cursor] == "month":
150 | display.line(ymd_offset + m_offset, 120, ymd_offset + m_offset + m_width, 120, 4)
151 | if cursors[cursor] == "day":
152 | display.line(ymd_offset + d_offset, 120, ymd_offset + d_offset + d_width, 120, 4)
153 |
154 | if cursors[cursor] == "hour":
155 | display.line(hms_offset, 70, hms_offset + h_width, 70, 4)
156 | if cursors[cursor] == "minute":
157 | display.line(hms_offset + mi_offset, 70, hms_offset + mi_offset + mi_width, 70, 4)
158 |
159 | display.set_update_speed(2)
160 | display.update()
161 | display.set_update_speed(3)
162 |
163 |
164 | def draw_second():
165 | global second_offset, second_unit_offset
166 |
167 | display.set_pen(15)
168 | display.rectangle(second_offset, 8, 75, 56)
169 | display.set_pen(0)
170 |
171 | if second // 10 != last_second // 10:
172 | s = "{:02}".format(second)
173 | display.text(s, second_offset, 40, 0, 1.8)
174 | display.partial_update(second_offset, 8, 75, 56)
175 |
176 | s = "{}".format(second // 10)
177 | second_unit_offset = second_offset + display.measure_text(s, 1.8)
178 |
179 | else:
180 | s = "{}".format(second % 10)
181 | display.text(s, second_unit_offset, 40, 0, 1.8)
182 | display.partial_update(second_unit_offset, 8, 75 - (second_unit_offset - second_offset), 56)
183 | time.sleep(0.9)
184 |
185 |
186 | for b in badger2040.BUTTONS.values():
187 | b.irq(trigger=machine.Pin.IRQ_RISING, handler=button)
188 |
189 | year, month, day, wd, hour, minute, second, _ = rtc.datetime()
190 |
191 | if (year, month, day) == (2021, 1, 1):
192 | rtc.datetime((2022, 2, 28, 0, 12, 0, 0, 0))
193 |
194 | last_second = second
195 | last_minute = minute
196 | draw_clock()
197 |
198 |
199 | while True:
200 | if not set_clock:
201 | year, month, day, wd, hour, minute, second, _ = rtc.datetime()
202 | if second != last_second:
203 | if minute != last_minute:
204 | draw_clock()
205 | last_minute = minute
206 | else:
207 | draw_second()
208 | last_second = second
209 |
210 | if toggle_set_clock:
211 | set_clock = not set_clock
212 | print(f"Set clock changed to: {set_clock}")
213 | toggle_set_clock = False
214 | draw_clock()
215 |
216 | time.sleep(0.01)
217 |
--------------------------------------------------------------------------------
/examples/ebook.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | import gc
3 | import badger_os
4 |
5 | # **** Put the name of your text file here *****
6 | text_file = "/books/289-0-wind-in-the-willows-abridged.txt" # File must be on the MicroPython device
7 |
8 | gc.collect()
9 |
10 | # Global Constants
11 | WIDTH = badger2040.WIDTH
12 | HEIGHT = badger2040.HEIGHT
13 |
14 | ARROW_THICKNESS = 3
15 | ARROW_WIDTH = 18
16 | ARROW_HEIGHT = 14
17 | ARROW_PADDING = 2
18 |
19 | TEXT_PADDING = 4
20 | TEXT_WIDTH = WIDTH - TEXT_PADDING - TEXT_PADDING - ARROW_WIDTH
21 |
22 | FONTS = ["sans", "gothic", "cursive", "serif"]
23 | THICKNESSES = [2, 1, 1, 2]
24 | # ------------------------------
25 | # Drawing functions
26 | # ------------------------------
27 |
28 |
29 | # Draw a upward arrow
30 | def draw_up(x, y, width, height, thickness, padding):
31 | border = (thickness // 4) + padding
32 | display.line(x + border, y + height - border,
33 | x + (width // 2), y + border)
34 | display.line(x + (width // 2), y + border,
35 | x + width - border, y + height - border)
36 |
37 |
38 | # Draw a downward arrow
39 | def draw_down(x, y, width, height, thickness, padding):
40 | border = (thickness // 2) + padding
41 | display.line(x + border, y + border,
42 | x + (width // 2), y + height - border)
43 | display.line(x + (width // 2), y + height - border,
44 | x + width - border, y + border)
45 |
46 |
47 | # Draw the frame of the reader
48 | def draw_frame():
49 | display.set_pen(15)
50 | display.clear()
51 | display.set_pen(12)
52 | display.rectangle(WIDTH - ARROW_WIDTH, 0, ARROW_WIDTH, HEIGHT)
53 | display.set_pen(0)
54 | if state["current_page"] > 0:
55 | draw_up(WIDTH - ARROW_WIDTH, (HEIGHT // 4) - (ARROW_HEIGHT // 2),
56 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
57 | draw_down(WIDTH - ARROW_WIDTH, ((HEIGHT * 3) // 4) - (ARROW_HEIGHT // 2),
58 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
59 |
60 |
61 | # ------------------------------
62 | # Program setup
63 | # ------------------------------
64 |
65 | # Global variables
66 | state = {
67 | "last_offset": 0,
68 | "current_page": 0,
69 | "font_idx": 0,
70 | "text_size": 0.5,
71 | "offsets": []
72 | }
73 | badger_os.state_load("ebook", state)
74 |
75 | text_spacing = int(34 * state["text_size"])
76 |
77 |
78 | # Create a new Badger and set it to update FAST
79 | display = badger2040.Badger2040()
80 | display.led(128)
81 | display.set_update_speed(badger2040.UPDATE_FAST)
82 |
83 |
84 | # ------------------------------
85 | # Render page
86 | # ------------------------------
87 |
88 | def render_page():
89 | row = 0
90 | line = ""
91 | pos = ebook.tell()
92 | next_pos = pos
93 | add_newline = False
94 | display.set_font(FONTS[state["font_idx"]])
95 | display.set_thickness(THICKNESSES[state["font_idx"]])
96 |
97 | while True:
98 | # Read a full line and split it into words
99 | words = ebook.readline().split(" ")
100 |
101 | # Take the length of the first word and advance our position
102 | next_word = words[0]
103 | if len(words) > 1:
104 | next_pos += len(next_word) + 1
105 | else:
106 | next_pos += len(next_word) # This is the last word on the line
107 |
108 | # Advance our position further if the word contains special characters
109 | if '\u201c' in next_word:
110 | next_word = next_word.replace('\u201c', '\"')
111 | next_pos += 2
112 | if '\u201d' in next_word:
113 | next_word = next_word.replace('\u201d', '\"')
114 | next_pos += 2
115 | if '\u2019' in next_word:
116 | next_word = next_word.replace('\u2019', '\'')
117 | next_pos += 2
118 |
119 | # Rewind the file back from the line end to the start of the next word
120 | ebook.seek(next_pos)
121 |
122 | # Strip out any new line characters from the word
123 | next_word = next_word.strip()
124 |
125 | # If an empty word is encountered assume that means there was a blank line
126 | if len(next_word) == 0:
127 | add_newline = True
128 |
129 | # Append the word to the current line and measure its length
130 | appended_line = line
131 | if len(line) > 0 and len(next_word) > 0:
132 | appended_line += " "
133 | appended_line += next_word
134 | appended_length = display.measure_text(appended_line, state["text_size"])
135 |
136 | # Would this appended line be longer than the text display area, or was a blank line spotted?
137 | if appended_length >= TEXT_WIDTH or add_newline:
138 |
139 | # Yes, so write out the line prior to the append
140 | print(line)
141 | display.set_pen(0)
142 | display.text(line, TEXT_PADDING, (row * text_spacing) + (text_spacing // 2) + TEXT_PADDING, WIDTH, state["text_size"])
143 |
144 | # Clear the line and move on to the next row
145 | line = ""
146 | row += 1
147 |
148 | # Have we reached the end of the page?
149 | if (row * text_spacing) + text_spacing >= HEIGHT:
150 | print("+++++")
151 | display.update()
152 |
153 | # Reset the position to the start of the word that made this line too long
154 | ebook.seek(pos)
155 | return
156 | else:
157 | # Set the line to the word and advance the current position
158 | line = next_word
159 | pos = next_pos
160 |
161 | # A new line was spotted, so advance a row
162 | if add_newline:
163 | print("")
164 | row += 1
165 | if (row * text_spacing) + text_spacing >= HEIGHT:
166 | print("+++++")
167 | display.update()
168 | return
169 | add_newline = False
170 | else:
171 | # The appended line was not too long, so set it as the line and advance the current position
172 | line = appended_line
173 | pos = next_pos
174 |
175 |
176 | # ------------------------------
177 | # Main program loop
178 | # ------------------------------
179 |
180 | launch = True
181 | changed = False
182 |
183 | # Open the book file
184 | ebook = open(text_file, "r")
185 | if len(state["offsets"]) > state["current_page"]:
186 | ebook.seek(state["offsets"][state["current_page"]])
187 | else:
188 | state["current_page"] = 0
189 | state["offsets"] = []
190 |
191 | while True:
192 | # Sometimes a button press or hold will keep the system
193 | # powered *through* HALT, so latch the power back on.
194 | display.keepalive()
195 |
196 | # Was the next page button pressed?
197 | if display.pressed(badger2040.BUTTON_DOWN):
198 | state["current_page"] += 1
199 |
200 | changed = True
201 |
202 | # Was the previous page button pressed?
203 | if display.pressed(badger2040.BUTTON_UP):
204 | if state["current_page"] > 0:
205 | state["current_page"] -= 1
206 | if state["current_page"] == 0:
207 | ebook.seek(0)
208 | else:
209 | ebook.seek(state["offsets"][state["current_page"] - 1]) # Retrieve the start position of the last page
210 | changed = True
211 |
212 | if display.pressed(badger2040.BUTTON_A):
213 | state["text_size"] += 0.1
214 | if state["text_size"] > 0.8:
215 | state["text_size"] = 0.5
216 | text_spacing = int(34 * state["text_size"])
217 | state["offsets"] = []
218 | ebook.seek(0)
219 | state["current_page"] = 0
220 | changed = True
221 |
222 | if display.pressed(badger2040.BUTTON_B):
223 | state["font_idx"] += 1
224 | if (state["font_idx"] >= len(FONTS)):
225 | state["font_idx"] = 0
226 | state["offsets"] = []
227 | ebook.seek(0)
228 | state["current_page"] = 0
229 | changed = True
230 |
231 | if launch and not changed:
232 | if state["current_page"] > 0 and len(state["offsets"]) > state["current_page"] - 1:
233 | ebook.seek(state["offsets"][state["current_page"] - 1])
234 | changed = True
235 | launch = False
236 |
237 | if changed:
238 | draw_frame()
239 | render_page()
240 |
241 | # Is the next page one we've not displayed before?
242 | if state["current_page"] >= len(state["offsets"]):
243 | state["offsets"].append(ebook.tell()) # Add its start position to the state["offsets"] list
244 | badger_os.state_save("ebook", state)
245 |
246 | changed = False
247 |
248 | display.halt()
249 |
--------------------------------------------------------------------------------
/examples/fonts.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | import badger_os
3 |
4 | # Global Constants
5 | FONT_NAMES = (
6 | ("sans", 0.7, 2),
7 | ("gothic", 0.7, 2),
8 | ("cursive", 0.7, 2),
9 | ("serif", 0.7, 2),
10 | ("serif_italic", 0.7, 2),
11 | ("bitmap6", 3, 1),
12 | ("bitmap8", 2, 1),
13 | ("bitmap14_outline", 1, 1)
14 | )
15 |
16 | WIDTH = badger2040.WIDTH
17 | HEIGHT = badger2040.HEIGHT
18 |
19 | MENU_TEXT_SIZE = 0.5
20 | MENU_SPACING = 16
21 | MENU_WIDTH = 84
22 | MENU_PADDING = 5
23 |
24 | TEXT_INDENT = MENU_WIDTH + 10
25 |
26 | ARROW_THICKNESS = 3
27 | ARROW_WIDTH = 18
28 | ARROW_HEIGHT = 14
29 | ARROW_PADDING = 2
30 |
31 |
32 | # ------------------------------
33 | # Drawing functions
34 | # ------------------------------
35 |
36 | # Draw a upward arrow
37 | def draw_up(x, y, width, height, thickness, padding):
38 | border = (thickness // 4) + padding
39 | display.line(x + border, y + height - border,
40 | x + (width // 2), y + border)
41 | display.line(x + (width // 2), y + border,
42 | x + width - border, y + height - border)
43 |
44 |
45 | # Draw a downward arrow
46 | def draw_down(x, y, width, height, thickness, padding):
47 | border = (thickness // 2) + padding
48 | display.line(x + border, y + border,
49 | x + (width // 2), y + height - border)
50 | display.line(x + (width // 2), y + height - border,
51 | x + width - border, y + border)
52 |
53 |
54 | # Draw the frame of the reader
55 | def draw_frame():
56 | display.set_pen(15)
57 | display.clear()
58 | display.set_pen(12)
59 | display.rectangle(WIDTH - ARROW_WIDTH, 0, ARROW_WIDTH, HEIGHT)
60 | display.set_pen(0)
61 | draw_up(WIDTH - ARROW_WIDTH, (HEIGHT // 4) - (ARROW_HEIGHT // 2),
62 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
63 | draw_down(WIDTH - ARROW_WIDTH, ((HEIGHT * 3) // 4) - (ARROW_HEIGHT // 2),
64 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
65 |
66 |
67 | # Draw the fonts and menu
68 | def draw_fonts():
69 | display.set_font("bitmap8")
70 | for i in range(len(FONT_NAMES)):
71 | name, size, thickness = FONT_NAMES[i]
72 | display.set_pen(0)
73 | if i == state["selected_font"]:
74 | display.rectangle(0, i * MENU_SPACING, MENU_WIDTH, MENU_SPACING)
75 | display.set_pen(15)
76 |
77 | display.text(name, MENU_PADDING, (i * MENU_SPACING) + int((MENU_SPACING - 8) / 2), WIDTH, MENU_TEXT_SIZE)
78 |
79 | name, size, thickness = FONT_NAMES[state["selected_font"]]
80 | display.set_font(name)
81 |
82 | y = 0 if name.startswith("bitmap") else 10
83 |
84 | display.set_pen(0)
85 | for line in ("The quick", "brown fox", "jumps over", "the lazy dog.", "0123456789", "!\"£$%^&*()"):
86 | display.text(line, TEXT_INDENT, y, WIDTH, size)
87 | y += 22
88 |
89 | display.update()
90 |
91 |
92 | # ------------------------------
93 | # Program setup
94 | # ------------------------------
95 |
96 | # Global variables
97 | state = {"selected_font": 0}
98 | badger_os.state_load("fonts", state)
99 |
100 | # Create a new Badger and set it to update FAST
101 | display = badger2040.Badger2040()
102 | display.led(128)
103 | display.set_update_speed(badger2040.UPDATE_FAST)
104 |
105 | changed = True
106 |
107 | # ------------------------------
108 | # Main program loop
109 | # ------------------------------
110 |
111 | while True:
112 | # Sometimes a button press or hold will keep the system
113 | # powered *through* HALT, so latch the power back on.
114 | display.keepalive()
115 |
116 | if display.pressed(badger2040.BUTTON_UP):
117 | state["selected_font"] -= 1
118 | if state["selected_font"] < 0:
119 | state["selected_font"] = len(FONT_NAMES) - 1
120 | changed = True
121 |
122 | if display.pressed(badger2040.BUTTON_DOWN):
123 | state["selected_font"] += 1
124 | if state["selected_font"] >= len(FONT_NAMES):
125 | state["selected_font"] = 0
126 | changed = True
127 |
128 | if changed:
129 | draw_frame()
130 | draw_fonts()
131 | badger_os.state_save("fonts", state)
132 | changed = False
133 |
134 | display.halt()
135 |
--------------------------------------------------------------------------------
/examples/help.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | from badger2040 import WIDTH
3 |
4 | TEXT_SIZE = 0.45
5 | LINE_HEIGHT = 20
6 |
7 | display = badger2040.Badger2040()
8 | display.led(128)
9 | display.set_thickness(2)
10 |
11 | # Clear to white
12 | display.set_pen(15)
13 | display.clear()
14 |
15 | display.set_font("bitmap8")
16 | display.set_pen(0)
17 | display.rectangle(0, 0, WIDTH, 16)
18 | display.set_pen(15)
19 | display.text("badgerOS", 3, 4, WIDTH, 1)
20 | display.text("help", WIDTH - display.measure_text("help", 0.4) - 4, 4, WIDTH, 1)
21 |
22 | display.set_font("sans")
23 | display.set_pen(0)
24 |
25 | TEXT_SIZE = 0.62
26 | y = 20 + int(LINE_HEIGHT / 2)
27 |
28 | display.set_font("sans")
29 | display.text("Up/Down - Change page", 0, y, WIDTH, TEXT_SIZE)
30 | y += LINE_HEIGHT
31 | display.text("a, b or c - Launch app", 0, y, WIDTH, TEXT_SIZE)
32 | y += LINE_HEIGHT
33 | display.text("a & c - Exit app", 0, y, WIDTH, TEXT_SIZE)
34 | y += LINE_HEIGHT
35 |
36 | display.update()
37 |
38 | # Call halt in a loop, on battery this switches off power.
39 | # On USB, the app will exit when A+C is pressed because the launcher picks that up.
40 | while True:
41 | display.keepalive()
42 | display.halt()
43 |
--------------------------------------------------------------------------------
/examples/icon-apps.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-apps.jpg
--------------------------------------------------------------------------------
/examples/icon-badge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-badge.jpg
--------------------------------------------------------------------------------
/examples/icon-cl1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-cl1.jpg
--------------------------------------------------------------------------------
/examples/icon-cl2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-cl2.jpg
--------------------------------------------------------------------------------
/examples/icon-clock.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-clock.jpg
--------------------------------------------------------------------------------
/examples/icon-dash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-dash.jpg
--------------------------------------------------------------------------------
/examples/icon-ebook.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-ebook.jpg
--------------------------------------------------------------------------------
/examples/icon-fonts.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-fonts.jpg
--------------------------------------------------------------------------------
/examples/icon-form.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-form.jpg
--------------------------------------------------------------------------------
/examples/icon-help.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-help.jpg
--------------------------------------------------------------------------------
/examples/icon-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-image.jpg
--------------------------------------------------------------------------------
/examples/icon-info.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-info.jpg
--------------------------------------------------------------------------------
/examples/icon-list.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-list.jpg
--------------------------------------------------------------------------------
/examples/icon-logger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-logger.jpg
--------------------------------------------------------------------------------
/examples/icon-net-info.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-net-info.jpg
--------------------------------------------------------------------------------
/examples/icon-news.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-news.jpg
--------------------------------------------------------------------------------
/examples/icon-power.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-power.jpg
--------------------------------------------------------------------------------
/examples/icon-qrgen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-qrgen.jpg
--------------------------------------------------------------------------------
/examples/icon-sendODK.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-sendODK.jpg
--------------------------------------------------------------------------------
/examples/icon-space.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-space.jpg
--------------------------------------------------------------------------------
/examples/icon-totp.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-totp.jpg
--------------------------------------------------------------------------------
/examples/icon-totp2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-totp2.jpg
--------------------------------------------------------------------------------
/examples/icon-weather.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/examples/icon-weather.jpg
--------------------------------------------------------------------------------
/examples/image.py:
--------------------------------------------------------------------------------
1 | import os
2 | import badger2040
3 | from badger2040 import HEIGHT, WIDTH
4 | import badger_os
5 | import jpegdec
6 |
7 |
8 | TOTAL_IMAGES = 0
9 |
10 |
11 | # Turn the act LED on as soon as possible
12 | display = badger2040.Badger2040()
13 | display.led(128)
14 | display.set_update_speed(badger2040.UPDATE_NORMAL)
15 |
16 | jpeg = jpegdec.JPEG(display.display)
17 |
18 |
19 | # Load images
20 | try:
21 | IMAGES = [f for f in os.listdir("/images") if f.endswith(".jpg")]
22 | TOTAL_IMAGES = len(IMAGES)
23 | except OSError:
24 | pass
25 |
26 |
27 | state = {
28 | "current_image": 0,
29 | "show_info": True
30 | }
31 |
32 |
33 | def show_image(n):
34 | file = IMAGES[n]
35 | name = file.split(".")[0]
36 | jpeg.open_file("/images/{}".format(file))
37 | jpeg.decode()
38 |
39 | if state["show_info"]:
40 | name_length = display.measure_text(name, 0.5)
41 | display.set_pen(0)
42 | display.rectangle(0, HEIGHT - 21, name_length + 11, 21)
43 | display.set_pen(15)
44 | display.rectangle(0, HEIGHT - 20, name_length + 10, 20)
45 | display.set_pen(0)
46 | display.text(name, 5, HEIGHT - 10, WIDTH, 0.5)
47 |
48 | for i in range(TOTAL_IMAGES):
49 | x = 286
50 | y = int((128 / 2) - (TOTAL_IMAGES * 10 / 2) + (i * 10))
51 | display.set_pen(0)
52 | display.rectangle(x, y, 8, 8)
53 | if state["current_image"] != i:
54 | display.set_pen(15)
55 | display.rectangle(x + 1, y + 1, 6, 6)
56 |
57 | display.update()
58 |
59 |
60 | if TOTAL_IMAGES == 0:
61 | raise RuntimeError("To run this demo, create an /images directory on your device and upload some 1bit 296x128 pixel images.")
62 |
63 |
64 | badger_os.state_load("image", state)
65 |
66 | changed = True
67 |
68 |
69 | while True:
70 | # Sometimes a button press or hold will keep the system
71 | # powered *through* HALT, so latch the power back on.
72 | display.keepalive()
73 |
74 | if display.pressed(badger2040.BUTTON_UP):
75 | if state["current_image"] > 0:
76 | state["current_image"] -= 1
77 | changed = True
78 |
79 | if display.pressed(badger2040.BUTTON_DOWN):
80 | if state["current_image"] < TOTAL_IMAGES - 1:
81 | state["current_image"] += 1
82 | changed = True
83 |
84 | if display.pressed(badger2040.BUTTON_A):
85 | state["show_info"] = not state["show_info"]
86 | changed = True
87 |
88 | if changed:
89 | show_image(state["current_image"])
90 | badger_os.state_save("image", state)
91 | changed = False
92 |
93 | # Halt the Badger to save power, it will wake up if any of the front buttons are pressed
94 | display.halt()
95 |
--------------------------------------------------------------------------------
/examples/info.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | from badger2040 import WIDTH
3 |
4 | TEXT_SIZE = 1
5 | LINE_HEIGHT = 15
6 |
7 | display = badger2040.Badger2040()
8 | display.led(128)
9 |
10 | # Clear to white
11 | display.set_pen(15)
12 | display.clear()
13 |
14 | display.set_font("bitmap8")
15 | display.set_pen(0)
16 | display.rectangle(0, 0, WIDTH, 16)
17 | display.set_pen(15)
18 | display.text("badgerOS", 3, 4, WIDTH, 1)
19 | display.text("info", WIDTH - display.measure_text("help", 0.4) - 4, 4, WIDTH, 1)
20 |
21 | display.set_pen(0)
22 |
23 | y = 16 + int(LINE_HEIGHT / 2)
24 |
25 | display.text("Made by Pimoroni, powered by MicroPython", 5, y, WIDTH, TEXT_SIZE)
26 | y += LINE_HEIGHT
27 | display.text("Dual-core RP2040, 133MHz, 264KB RAM", 5, y, WIDTH, TEXT_SIZE)
28 | y += LINE_HEIGHT
29 | display.text("2MB Flash (1MB OS, 1MB Storage)", 5, y, WIDTH, TEXT_SIZE)
30 | y += LINE_HEIGHT
31 | display.text("296x128 pixel Black/White e-Ink", 5, y, WIDTH, TEXT_SIZE)
32 | y += LINE_HEIGHT
33 | y += LINE_HEIGHT
34 |
35 | display.text("For more info:", 5, y, WIDTH, TEXT_SIZE)
36 | y += LINE_HEIGHT
37 | display.text("https://pimoroni.com/badger2040", 5, y, WIDTH, TEXT_SIZE)
38 |
39 | display.update()
40 |
41 | # Call halt in a loop, on battery this switches off power.
42 | # On USB, the app will exit when A+C is pressed because the launcher picks that up.
43 | while True:
44 | display.keepalive()
45 | display.halt()
46 |
--------------------------------------------------------------------------------
/examples/list.py:
--------------------------------------------------------------------------------
1 | import binascii
2 |
3 | import badger2040
4 | import badger_os
5 |
6 | # **** Put your list title here *****
7 | list_title = "Checklist"
8 | list_file = "checklist.txt"
9 |
10 |
11 | # Global Constantsu
12 | WIDTH = badger2040.WIDTH
13 | HEIGHT = badger2040.HEIGHT
14 |
15 | ARROW_THICKNESS = 3
16 | ARROW_WIDTH = 18
17 | ARROW_HEIGHT = 14
18 | ARROW_PADDING = 2
19 |
20 | MAX_ITEM_CHARS = 26
21 | TITLE_TEXT_SIZE = 0.7
22 | ITEM_TEXT_SIZE = 0.6
23 | ITEM_SPACING = 20
24 |
25 | LIST_START = 40
26 | LIST_PADDING = 2
27 | LIST_WIDTH = WIDTH - LIST_PADDING - LIST_PADDING - ARROW_WIDTH
28 | LIST_HEIGHT = HEIGHT - LIST_START - LIST_PADDING - ARROW_HEIGHT
29 |
30 |
31 | # Default list items - change the list items by editing checklist.txt
32 | list_items = ["Badger", "Badger", "Badger", "Badger", "Badger", "Mushroom", "Mushroom", "Snake"]
33 | save_checklist = False
34 |
35 | try:
36 | with open("checklist.txt", "r") as f:
37 | raw_list_items = f.read()
38 |
39 | if raw_list_items.find(" X\n") != -1:
40 | # Have old style checklist, preserve state and note we should resave the list to remove the Xs
41 | list_items = []
42 | state = {
43 | "current_item": 0,
44 | "checked": []
45 | }
46 | for item in raw_list_items.strip().split("\n"):
47 | if item.endswith(" X"):
48 | state["checked"].append(True)
49 | item = item[:-2]
50 | else:
51 | state["checked"].append(False)
52 | list_items.append(item)
53 | state["items_hash"] = binascii.crc32("\n".join(list_items))
54 |
55 | badger_os.state_save("list", state)
56 | save_checklist = True
57 | else:
58 | list_items = [item.strip() for item in raw_list_items.strip().split("\n")]
59 |
60 | except OSError:
61 | save_checklist = True
62 |
63 | if save_checklist:
64 | with open("checklist.txt", "w") as f:
65 | for item in list_items:
66 | f.write(item + "\n")
67 |
68 |
69 | # ------------------------------
70 | # Drawing functions
71 | # ------------------------------
72 |
73 | # Draw the list of items
74 | def draw_list(items, item_states, start_item, highlighted_item, x, y, width, height, item_height, columns):
75 | item_x = 0
76 | item_y = 0
77 | current_col = 0
78 | for i in range(start_item, len(items)):
79 | if i == highlighted_item:
80 | display.set_pen(12)
81 | display.rectangle(item_x, item_y + y - (item_height // 2), width // columns, item_height)
82 | display.set_pen(0)
83 | display.text(items[i], item_x + x + item_height, item_y + y, WIDTH, ITEM_TEXT_SIZE)
84 | draw_checkbox(item_x, item_y + y - (item_height // 2), item_height, 15, 0, 2, item_states[i], 2)
85 | item_y += item_height
86 | if item_y >= height - (item_height // 2):
87 | item_x += width // columns
88 | item_y = 0
89 | current_col += 1
90 | if current_col >= columns:
91 | return
92 |
93 |
94 | # Draw a upward arrow
95 | def draw_up(x, y, width, height, thickness, padding):
96 | border = (thickness // 4) + padding
97 | display.line(x + border, y + height - border,
98 | x + (width // 2), y + border)
99 | display.line(x + (width // 2), y + border,
100 | x + width - border, y + height - border)
101 |
102 |
103 | # Draw a downward arrow
104 | def draw_down(x, y, width, height, thickness, padding):
105 | border = (thickness // 2) + padding
106 | display.line(x + border, y + border,
107 | x + (width // 2), y + height - border)
108 | display.line(x + (width // 2), y + height - border,
109 | x + width - border, y + border)
110 |
111 |
112 | # Draw a left arrow
113 | def draw_left(x, y, width, height, thickness, padding):
114 | border = (thickness // 2) + padding
115 | display.line(x + width - border, y + border,
116 | x + border, y + (height // 2))
117 | display.line(x + border, y + (height // 2),
118 | x + width - border, y + height - border)
119 |
120 |
121 | # Draw a right arrow
122 | def draw_right(x, y, width, height, thickness, padding):
123 | border = (thickness // 2) + padding
124 | display.line(x + border, y + border,
125 | x + width - border, y + (height // 2))
126 | display.line(x + width - border, y + (height // 2),
127 | x + border, y + height - border)
128 |
129 |
130 | # Draw a tick
131 | def draw_tick(x, y, width, height, thickness, padding):
132 | border = (thickness // 2) + padding
133 | display.line(x + border, y + ((height * 2) // 3),
134 | x + (width // 2), y + height - border)
135 | display.line(x + (width // 2), y + height - border,
136 | x + width - border, y + border)
137 |
138 |
139 | # Draw a cross
140 | def draw_cross(x, y, width, height, thickness, padding):
141 | border = (thickness // 2) + padding
142 | display.line(x + border, y + border, x + width - border, y + height - border)
143 | display.line(x + width - border, y + border, x + border, y + height - border)
144 |
145 |
146 | # Draw a checkbox with or without a tick
147 | def draw_checkbox(x, y, size, background, foreground, thickness, tick, padding):
148 | border = (thickness // 2) + padding
149 | display.set_pen(background)
150 | display.rectangle(x + border, y + border, size - (border * 2), size - (border * 2))
151 | display.set_pen(foreground)
152 | display.line(x + border, y + border, x + size - border, y + border)
153 | display.line(x + border, y + border, x + border, y + size - border)
154 | display.line(x + size - border, y + border, x + size - border, y + size - border)
155 | display.line(x + border, y + size - border, x + size - border, y + size - border)
156 | if tick:
157 | draw_tick(x, y, size, size, thickness, 2 + border)
158 |
159 |
160 | # ------------------------------
161 | # Program setup
162 | # ------------------------------
163 |
164 | changed = True
165 | state = {
166 | "current_item": 0,
167 | }
168 | badger_os.state_load("list", state)
169 | items_hash = binascii.crc32("\n".join(list_items))
170 | if "items_hash" not in state or state["items_hash"] != items_hash:
171 | # Item list changed, or not yet written reset the list
172 | state["current_item"] = 0
173 | state["items_hash"] = items_hash
174 | state["checked"] = [False] * len(list_items)
175 | changed = True
176 |
177 | # Global variables
178 | items_per_page = 0
179 |
180 | # Create a new Badger and set it to update FAST
181 | display = badger2040.Badger2040()
182 | display.led(128)
183 | display.set_font("sans")
184 | display.set_thickness(2)
185 | if changed:
186 | display.set_update_speed(badger2040.UPDATE_FAST)
187 | else:
188 | display.set_update_speed(badger2040.UPDATE_TURBO)
189 |
190 | # Find out what the longest item is
191 | longest_item = 0
192 | for i in range(len(list_items)):
193 | while True:
194 | item = list_items[i]
195 | item_length = display.measure_text(item, ITEM_TEXT_SIZE)
196 | if item_length > 0 and item_length > LIST_WIDTH - ITEM_SPACING:
197 | list_items[i] = item[:-1]
198 | else:
199 | break
200 | longest_item = max(longest_item, display.measure_text(list_items[i], ITEM_TEXT_SIZE))
201 |
202 |
203 | # And use that to calculate the number of columns we can fit onscreen and how many items that would give
204 | list_columns = 1
205 | while longest_item + ITEM_SPACING < (LIST_WIDTH // (list_columns + 1)):
206 | list_columns += 1
207 |
208 | items_per_page = ((LIST_HEIGHT // ITEM_SPACING) + 1) * list_columns
209 |
210 |
211 | # ------------------------------
212 | # Main program loop
213 | # ------------------------------
214 |
215 | while True:
216 | # Sometimes a button press or hold will keep the system
217 | # powered *through* HALT, so latch the power back on.
218 | display.keepalive()
219 |
220 | if len(list_items) > 0:
221 | if display.pressed(badger2040.BUTTON_A):
222 | if state["current_item"] > 0:
223 | page = state["current_item"] // items_per_page
224 | state["current_item"] = max(state["current_item"] - (items_per_page) // list_columns, 0)
225 | if page != state["current_item"] // items_per_page:
226 | display.update_speed(badger2040.UPDATE_FAST)
227 | changed = True
228 | if display.pressed(badger2040.BUTTON_B):
229 | state["checked"][state["current_item"]] = not state["checked"][state["current_item"]]
230 | changed = True
231 | if display.pressed(badger2040.BUTTON_C):
232 | if state["current_item"] < len(list_items) - 1:
233 | page = state["current_item"] // items_per_page
234 | state["current_item"] = min(state["current_item"] + (items_per_page) // list_columns, len(list_items) - 1)
235 | if page != state["current_item"] // items_per_page:
236 | display.update_speed(badger2040.UPDATE_FAST)
237 | changed = True
238 | if display.pressed(badger2040.BUTTON_UP):
239 | if state["current_item"] > 0:
240 | state["current_item"] -= 1
241 | changed = True
242 | if display.pressed(badger2040.BUTTON_DOWN):
243 | if state["current_item"] < len(list_items) - 1:
244 | state["current_item"] += 1
245 | changed = True
246 |
247 | if changed:
248 | badger_os.state_save("list", state)
249 |
250 | display.set_pen(15)
251 | display.clear()
252 |
253 | display.set_pen(12)
254 | display.rectangle(WIDTH - ARROW_WIDTH, 0, ARROW_WIDTH, HEIGHT)
255 | display.rectangle(0, HEIGHT - ARROW_HEIGHT, WIDTH, ARROW_HEIGHT)
256 |
257 | y = LIST_PADDING + 12
258 | display.set_pen(0)
259 | display.text(list_title, LIST_PADDING, y, WIDTH, TITLE_TEXT_SIZE)
260 |
261 | y += 12
262 | display.set_pen(0)
263 | display.line(LIST_PADDING, y, WIDTH - LIST_PADDING - ARROW_WIDTH, y)
264 |
265 | if len(list_items) > 0:
266 | page_item = 0
267 | if items_per_page > 0:
268 | page_item = (state["current_item"] // items_per_page) * items_per_page
269 |
270 | # Draw the list
271 | display.set_pen(0)
272 | draw_list(list_items, state["checked"], page_item, state["current_item"], LIST_PADDING, LIST_START,
273 | LIST_WIDTH, LIST_HEIGHT, ITEM_SPACING, list_columns)
274 |
275 | # Draw the interaction button icons
276 | display.set_pen(0)
277 |
278 | # Previous item
279 | if state["current_item"] > 0:
280 | draw_up(WIDTH - ARROW_WIDTH, (HEIGHT // 4) - (ARROW_HEIGHT // 2),
281 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
282 |
283 | # Next item
284 | if state["current_item"] < (len(list_items) - 1):
285 | draw_down(WIDTH - ARROW_WIDTH, ((HEIGHT * 3) // 4) - (ARROW_HEIGHT // 2),
286 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
287 |
288 | # Previous column
289 | if state["current_item"] > 0:
290 | draw_left((WIDTH // 7) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT,
291 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
292 |
293 | # Next column
294 | if state["current_item"] < (len(list_items) - 1):
295 | draw_right(((WIDTH * 6) // 7) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT,
296 | ARROW_WIDTH, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
297 |
298 | if state["checked"][state["current_item"]]:
299 | # Tick off item
300 | draw_cross((WIDTH // 2) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT,
301 | ARROW_HEIGHT, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
302 | else:
303 | # Untick item
304 | draw_tick((WIDTH // 2) - (ARROW_WIDTH // 2), HEIGHT - ARROW_HEIGHT,
305 | ARROW_HEIGHT, ARROW_HEIGHT, ARROW_THICKNESS, ARROW_PADDING)
306 | else:
307 | # Say that the list is empty
308 | empty_text = "Nothing Here"
309 | text_length = display.measure_text(empty_text, ITEM_TEXT_SIZE)
310 | display.text(empty_text, ((LIST_PADDING + LIST_WIDTH) - text_length) // 2, (LIST_HEIGHT // 2) + LIST_START - (ITEM_SPACING // 4), WIDTH, ITEM_TEXT_SIZE)
311 |
312 | display.update()
313 | display.set_update_speed(badger2040.UPDATE_TURBO)
314 | changed = False
315 |
316 | display.halt()
317 |
--------------------------------------------------------------------------------
/examples/logger.py:
--------------------------------------------------------------------------------
1 | import utime
2 | from machine import Pin, I2C
3 | import machine
4 | import ahtx0
5 | import badger2040
6 | from badger2040 import WIDTH, HEIGHT
7 | import os
8 | from pcf85063a import PCF85063A
9 |
10 | # ==== CONFIGURATION ====
11 | csv_file_path = "data/logged_data.csv"
12 | LOG_INTERVAL = 1800 # seconds between measurements
13 | y_scale = 100
14 |
15 | # ==== INIT HARDWARE ====
16 | display = badger2040.Badger2040()
17 | display.set_thickness(4)
18 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
19 | rtc = PCF85063A(i2c)
20 | sensor = ahtx0.AHT20(i2c)
21 |
22 | # ==== FILESYSTEM ====
23 | try:
24 | os.mkdir("data")
25 | except OSError as e:
26 | if e.args[0] != 17:
27 | raise
28 |
29 | # ==== FUNCTIONS ====
30 |
31 | def clear():
32 | display.set_pen(15)
33 | display.clear()
34 | display.set_font("bitmap8")
35 |
36 | # Top and bottom bars
37 | display.set_pen(0)
38 | display.rectangle(0, 0, WIDTH, 10)
39 | display.rectangle(0, HEIGHT - 10, WIDTH, 10)
40 |
41 | # White title on top bar
42 | display.set_pen(15)
43 | display.text("Temp/Humidity Logger", 10, 1, WIDTH, 0.6)
44 |
45 | def get_iso_timestamp():
46 | now = rtc.datetime()
47 | return "{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}".format(*now[:6])
48 |
49 | def read_last_n_entries_from_csv(n=50):
50 | ts, temps, hums = [], [], []
51 | try:
52 | with open(csv_file_path, "r") as f:
53 | lines = f.readlines()[-n:]
54 | for line in lines:
55 | parts = line.strip().split(',')
56 | if len(parts) == 3:
57 | try:
58 | tstamp, temp, hum = parts
59 | ts.append(tstamp)
60 | temps.append(float(temp))
61 | hums.append(float(hum))
62 | except ValueError:
63 | continue
64 | except OSError as e:
65 | if e.args[0] != 2:
66 | raise
67 | return ts, temps, hums
68 |
69 | # ==== CHART AREA ====
70 | chart_width = 200
71 | chart_height = 80
72 | chart_origin_x = int(0.5 * (WIDTH - chart_width))
73 | chart_origin_y = int(0.5 * (HEIGHT - chart_height))
74 | legend_origin_x = chart_origin_x + chart_width + 20
75 | legend_origin_y = chart_origin_y
76 |
77 | # ==== MAIN LOOP ====
78 | try:
79 | while True:
80 | # === Read sensor ===
81 | temperature = sensor.temperature
82 | humidity = sensor.relative_humidity
83 | timestamp = get_iso_timestamp()
84 |
85 | # === Log to file ===
86 | with open(csv_file_path, "a") as f:
87 | f.write(f"{timestamp}, {temperature:.2f}, {humidity:.2f}\n")
88 |
89 | # === Read data ===
90 | _, temperature_values, humidity_values = read_last_n_entries_from_csv()
91 | num_points = len(temperature_values)
92 |
93 | # === Handle y_scale button ===
94 | if display.pressed(badger2040.BUTTON_UP):
95 | y_scale = {100: 80, 80: 60, 60: 50, 50: 40}.get(y_scale, 100)
96 | print("Changed y_scale to:", y_scale)
97 | utime.sleep_ms(200)
98 |
99 | # === Full clear and redraw ===
100 | display.set_update_speed(badger2040.UPDATE_NORMAL)
101 | display.set_pen(15)
102 | display.clear()
103 | clear()
104 |
105 | # === Axes ===
106 | display.set_pen(0)
107 | display.line(chart_origin_x, chart_origin_y + chart_height,
108 | chart_origin_x + chart_width, chart_origin_y + chart_height)
109 | display.line(chart_origin_x, chart_origin_y,
110 | chart_origin_x, chart_origin_y + chart_height)
111 |
112 | # === Y-ticks ===
113 | for i in range(0, y_scale + 1, 10):
114 | tick_y = chart_origin_y + chart_height - int(i * chart_height / y_scale)
115 | display.line(chart_origin_x - 3, tick_y, chart_origin_x + 3, tick_y)
116 | display.text(f"{i}", chart_origin_x - 25, tick_y - 4, WIDTH, 0.5)
117 |
118 | # === Legend ===
119 | display.set_pen(0)
120 | display.rectangle(legend_origin_x - 5, legend_origin_y, 15, 8)
121 | display.text("Temp", legend_origin_x + 12, legend_origin_y, WIDTH, 0.5)
122 |
123 | display.set_pen(4)
124 | display.rectangle(legend_origin_x - 5, legend_origin_y + 25, 15, 8)
125 | display.text("RH %", legend_origin_x + 12, legend_origin_y + 25, WIDTH, 0.5)
126 |
127 | # === Plot bars ===
128 | if num_points > 0:
129 | bar_unit = chart_width / num_points
130 | for i in range(num_points):
131 | x_base = chart_origin_x + int(i * bar_unit)
132 | temp_val = min(temperature_values[i], y_scale)
133 | hum_val = min(humidity_values[i], y_scale)
134 |
135 | temp_height = int(temp_val * chart_height / y_scale)
136 | hum_height = int(hum_val * chart_height / y_scale)
137 |
138 | # Temp bar (left half)
139 | display.set_pen(0)
140 | display.rectangle(x_base, chart_origin_y + chart_height - temp_height,
141 | int(bar_unit // 2), temp_height)
142 |
143 | # RH bar (right half)
144 | display.set_pen(4)
145 | display.rectangle(x_base + int(bar_unit // 2),
146 | chart_origin_y + chart_height - hum_height,
147 | int(bar_unit // 2), hum_height)
148 |
149 | # === Summary stats ===
150 | if temperature_values:
151 | avg_temp = sum(temperature_values) / len(temperature_values)
152 | avg_hum = sum(humidity_values) / len(humidity_values)
153 |
154 | # White text over black bars
155 | display.set_pen(15)
156 | display.text(f"Now: {temperature:.1f}°C | {humidity:.1f}%RH", 170, 1, WIDTH, 0.6)
157 | display.text(f"Avg: {avg_temp:.1f}°C | {avg_hum:.1f}%RH", 150, HEIGHT - 9, WIDTH, 0.6)
158 | display.text(timestamp, 10, HEIGHT - 9, WIDTH, 0.6)
159 |
160 | # === Show display ===
161 | display.update()
162 |
163 | # === Wait for next measurement ===
164 | utime.sleep(LOG_INTERVAL)
165 |
166 | except KeyboardInterrupt:
167 | pass
168 |
169 |
170 |
171 |
--------------------------------------------------------------------------------
/examples/net_info.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | from badger2040 import WIDTH
3 | import network
4 |
5 | TEXT_SIZE = 1
6 | LINE_HEIGHT = 16
7 |
8 | # Display Setup
9 | display = badger2040.Badger2040()
10 | display.led(128)
11 |
12 | # Connects to the wireless network. Ensure you have entered your details in WIFI_CONFIG.py :).
13 | display.connect()
14 | net = network.WLAN(network.STA_IF).ifconfig()
15 |
16 | # Page Header
17 | display.set_pen(15)
18 | display.clear()
19 | display.set_pen(0)
20 |
21 | display.set_pen(0)
22 | display.rectangle(0, 0, WIDTH, 20)
23 | display.set_pen(15)
24 | display.text("badgerOS", 3, 4)
25 | display.text("Network Details", WIDTH - display.measure_text("Network Details") - 4, 4)
26 | display.set_pen(0)
27 |
28 | y = 35 + int(LINE_HEIGHT / 2)
29 |
30 | if net:
31 | display.text("> LOCAL IP: {}".format(net[0]), 0, y, WIDTH)
32 | y += LINE_HEIGHT
33 | display.text("> Subnet: {}".format(net[1]), 0, y, WIDTH)
34 | y += LINE_HEIGHT
35 | display.text("> Gateway: {}".format(net[2]), 0, y, WIDTH)
36 | y += LINE_HEIGHT
37 | display.text("> DNS: {}".format(net[3]), 0, y, WIDTH)
38 | else:
39 | display.text("> No network connection!", 0, y, WIDTH)
40 | y += LINE_HEIGHT
41 | display.text("> Check details in WIFI_CONFIG.py", 0, y, WIDTH)
42 |
43 | display.update()
44 |
45 | # Call halt in a loop, on battery this switches off power.
46 | # On USB, the app will exit when A+C is pressed because the launcher picks that up.
47 | while True:
48 | display.keepalive()
49 | display.halt()
50 |
--------------------------------------------------------------------------------
/examples/news.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | from badger2040 import WIDTH
3 | import machine
4 | from urllib import urequest
5 | import gc
6 | import qrcode
7 | import badger_os
8 |
9 | # URLS to use (Entertainment, Science and Technology)
10 | URL = ["http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml",
11 | "http://feeds.bbci.co.uk/news/science_and_environment/rss.xml",
12 | "http://feeds.bbci.co.uk/news/technology/rss.xml"]
13 |
14 | code = qrcode.QRCode()
15 |
16 | state = {
17 | "current_page": 0,
18 | "feed": 2
19 | }
20 |
21 | badger_os.state_load("news", state)
22 |
23 | # Display Setup
24 | display = badger2040.Badger2040()
25 | display.led(128)
26 | display.set_update_speed(2)
27 |
28 | # Setup buttons
29 | button_a = machine.Pin(badger2040.BUTTON_A, machine.Pin.IN, machine.Pin.PULL_DOWN)
30 | button_b = machine.Pin(badger2040.BUTTON_B, machine.Pin.IN, machine.Pin.PULL_DOWN)
31 | button_c = machine.Pin(badger2040.BUTTON_C, machine.Pin.IN, machine.Pin.PULL_DOWN)
32 | button_down = machine.Pin(badger2040.BUTTON_DOWN, machine.Pin.IN, machine.Pin.PULL_DOWN)
33 | button_up = machine.Pin(badger2040.BUTTON_UP, machine.Pin.IN, machine.Pin.PULL_DOWN)
34 |
35 |
36 | def read_until(stream, char):
37 | result = b""
38 | while True:
39 | c = stream.read(1)
40 | if c == char:
41 | return result
42 | result += c
43 |
44 |
45 | def discard_until(stream, c):
46 | while stream.read(1) != c:
47 | pass
48 |
49 |
50 | def parse_xml_stream(s, accept_tags, group_by, max_items=3):
51 | tag = []
52 | text = b""
53 | count = 0
54 | current = {}
55 | while True:
56 | char = s.read(1)
57 | if len(char) == 0:
58 | break
59 |
60 | if char == b"<":
61 | next_char = s.read(1)
62 |
63 | # Discard stuff like ")
66 | continue
67 |
68 | # Detect ") # Discard ]>
74 | gc.collect()
75 |
76 | elif next_char == b"/":
77 | current_tag = read_until(s, b">")
78 | top_tag = tag[-1]
79 |
80 | # Populate our result dict
81 | if top_tag in accept_tags:
82 | current[top_tag.decode("utf-8")] = text.decode("utf-8")
83 |
84 | # If we've found a group of items, yield the dict
85 | elif top_tag == group_by:
86 | yield current
87 | current = {}
88 | count += 1
89 | if count == max_items:
90 | return
91 | tag.pop()
92 | text = b""
93 | gc.collect()
94 | continue
95 |
96 | else:
97 | current_tag = read_until(s, b">")
98 | tag += [next_char + current_tag.split(b" ")[0]]
99 | text = b""
100 | gc.collect()
101 |
102 | else:
103 | text += char
104 |
105 |
106 | def measure_qr_code(size, code):
107 | w, h = code.get_size()
108 | module_size = int(size / w)
109 | return module_size * w, module_size
110 |
111 |
112 | def draw_qr_code(ox, oy, size, code):
113 | size, module_size = measure_qr_code(size, code)
114 | display.set_pen(15)
115 | display.rectangle(ox, oy, size, size)
116 | display.set_pen(0)
117 | for x in range(size):
118 | for y in range(size):
119 | if code.get_module(x, y):
120 | display.rectangle(ox + x * module_size, oy + y * module_size, module_size, module_size)
121 |
122 |
123 | # A function to get the data from an RSS Feed, this in case BBC News.
124 | def get_rss(url):
125 | try:
126 | stream = urequest.urlopen(url)
127 | output = list(parse_xml_stream(stream, [b"title", b"description", b"guid", b"pubDate"], b"item"))
128 | return output
129 |
130 | except OSError as e:
131 | print(e)
132 | return False
133 |
134 |
135 | # Connects to the wireless network. Ensure you have entered your details in WIFI_CONFIG.py :).
136 | display.connect()
137 |
138 | print(state["feed"])
139 | feed = get_rss(URL[state["feed"]])
140 |
141 |
142 | def draw_page():
143 |
144 | # Clear the display
145 | display.set_pen(15)
146 | display.clear()
147 | display.set_pen(0)
148 |
149 | # Draw the page header
150 | display.set_font("bitmap6")
151 | display.set_pen(0)
152 | display.rectangle(0, 0, WIDTH, 20)
153 | display.set_pen(15)
154 | display.text("News", 3, 4)
155 | display.text("Page: " + str(state["current_page"] + 1), WIDTH - display.measure_text("Page: ") - 4, 4)
156 | display.set_pen(0)
157 |
158 | display.set_font("bitmap8")
159 |
160 | # Draw articles from the feed if they're available.
161 | if feed:
162 | page = state["current_page"]
163 | display.set_pen(0)
164 | display.text(feed[page]["title"], 2, 30, WIDTH - 130, 2)
165 | code.set_text(feed[page]["guid"])
166 | draw_qr_code(WIDTH - 100, 25, 100, code)
167 |
168 | else:
169 | display.set_pen(0)
170 | display.rectangle(0, 60, WIDTH, 25)
171 | display.set_pen(15)
172 | display.text("Unable to display news! Check your network settings in WIFI_CONFIG.py", 5, 65, WIDTH, 1)
173 |
174 | display.update()
175 |
176 |
177 | draw_page()
178 |
179 | while True:
180 | changed = False
181 |
182 | if button_down.value():
183 | if state["current_page"] < 2:
184 | state["current_page"] += 1
185 | changed = True
186 |
187 | if button_up.value():
188 | if state["current_page"] > 0:
189 | state["current_page"] -= 1
190 | changed = True
191 |
192 | if button_a.value():
193 | state["feed"] = 0
194 | state["current_page"] = 0
195 | feed = get_rss(URL[state["feed"]])
196 | badger_os.state_save("news", state)
197 | changed = True
198 |
199 | if button_b.value():
200 | state["feed"] = 1
201 | state["current_page"] = 0
202 | feed = get_rss(URL[state["feed"]])
203 | badger_os.state_save("news", state)
204 | changed = True
205 |
206 | if button_c.value():
207 | state["feed"] = 2
208 | state["current_page"] = 0
209 | feed = get_rss(URL[state["feed"]])
210 | badger_os.state_save("news", state)
211 | changed = True
212 |
213 | if changed:
214 | draw_page()
215 |
--------------------------------------------------------------------------------
/examples/power.py:
--------------------------------------------------------------------------------
1 | import machine
2 | import badger2040
3 | import badger_os #https://github.com/pimoroni/badger2040/blob/main/firmware/PIMORONI_BADGER2040/lib/badger_os.py
4 | import utime
5 | import network
6 | from machine import ADC, Pin
7 |
8 | #####################################
9 | # Define functions
10 | #####################################
11 |
12 | def voltogetter(pin):
13 | # Create an ADC object
14 | adc = machine.ADC(machine.Pin(pin)) # Replace 26 with the appropriate ADC pin number
15 |
16 | # Read the ADC raw value
17 | adc_value = adc.read_u16()
18 |
19 | # Get the reference voltage (assuming 3.3V)
20 | reference_voltage = 4.9
21 |
22 | # Convert ADC value to voltage
23 | voltage = adc_value / 65535 * reference_voltage
24 | return voltage
25 |
26 | def cls():
27 | display.set_pen(15)
28 | display.clear()
29 | display.set_pen(1)
30 | display.update()
31 |
32 | def get_battery_info(full_battery=3.7, empty_battery=2.8):
33 | # Pico W voltage read function by darconeous on reddit:
34 | # https://www.reddit.com/r/raspberrypipico/comments/xalach/comment/ipigfzu/
35 |
36 | # Initialize Variables
37 | conversion_factor = 3 * full_battery / 65535
38 | voltage=0
39 | percentage=0
40 | is_charge=False
41 | error_message=None
42 |
43 | # prep the network
44 | wlan = network.WLAN(network.STA_IF)
45 | wlan_active = wlan.active()
46 |
47 | try:
48 | # Don't use the WLAN chip for a moment.
49 | wlan.active(False)
50 |
51 | # Make sure pin 25 is high.
52 | Pin(25, mode=Pin.OUT, pull=Pin.PULL_DOWN).high()
53 |
54 | # Reconfigure pin 29 as an input.
55 | Pin(29, Pin.IN)
56 |
57 | vsys = ADC(29)
58 |
59 | # get the voltage
60 | voltage=vsys.read_u16() * conversion_factor
61 |
62 | # figure out the percentage of available battery
63 | if voltage:
64 | try:
65 | percentage = 100 * ((voltage - empty_battery) / (full_battery - empty_battery))
66 | except:
67 | percentage = 0
68 |
69 | if percentage > 100:
70 | percentage = 100.00
71 | elif percentage < 0:
72 | percentage = 0
73 |
74 | charging = Pin('WL_GPIO2', Pin.IN) # reading this pin tells us whether or not USB power is connected
75 | is_charge = charging.value()
76 |
77 | except Exception as e:
78 | error_message=str(e)
79 |
80 | finally:
81 | # Restore the pin state and possibly reactivate WLAN
82 | Pin(29, Pin.ALT, pull=Pin.PULL_DOWN, alt=7)
83 | wlan.active(wlan_active)
84 |
85 | return {"error":error_message, "voltage":voltage, "percentage":percentage, "full_battery":full_battery, "empty_battery":empty_battery, "is_charge":is_charge}
86 |
87 |
88 | #####################################
89 | # Find values
90 | #####################################
91 |
92 | # Get the voltage values for all pins first
93 | pin26 = round(voltogetter(26),2)
94 | pin27 = round(voltogetter(27),2)
95 | pin28 = round(voltogetter(28),2)
96 | pin29 = round(voltogetter(29),2)
97 |
98 | # Clear the screen
99 | print("clearing screen")
100 |
101 | #####################################
102 | # Display stats
103 | #####################################
104 | #
105 | # Initialise the badger screen
106 | display = badger2040.Badger2040()
107 | WIDTH = badger2040.WIDTH # 296
108 | HEIGHT = badger2040.HEIGHT # 128
109 |
110 | batlevel = get_battery_info()
111 | print(f"battery: {batlevel}%")
112 | diskusage = badger_os.get_disk_usage()
113 | print(f"diskusage: {diskusage}%")
114 | staterunning = badger_os.state_running()
115 | print(f"staterunning: {staterunning}%")
116 | print(get_battery_info())
117 | print(f"Battery 2 : {get_battery_info()}")
118 | print(batlevel['voltage'])
119 | # Get the CPU frequency
120 | cpu_freq = round(machine.freq()/1000000,0)
121 |
122 | cls()
123 | # Draw a grid
124 | # Vertical lines
125 | display.line(85, 0, 85, 70, 3)
126 | display.line(50, 70, 50, 150, 3)
127 | display.line(200, 70, 200, 150, 3)
128 |
129 | display.line(0, 40, 300, 40, 3)
130 | display.line(0, 70, 300, 70, 3)
131 |
132 | # Add voltage across various pins
133 | display.text(f"Voltage ", 0, 15,WIDTH,2)
134 | display.text(f"26: {pin26} V | 27: {pin27} V", 95, 5,WIDTH,2)
135 | display.text(f"28: {pin28} V | 29: {pin28} V", 95, 20,WIDTH,2)
136 |
137 | # Show the battery state
138 | display.text(f"Battery ",0, 50,WIDTH,2)
139 | display.text(f"{round(batlevel['voltage'],2)}V | {round(batlevel['percentage'],2)}%",95, 50,WIDTH,2)
140 |
141 | # Show the disk space used/available
142 | display.text(f"Disk", 0, 90,WIDTH,2)
143 | display.text(f" Total : {round(diskusage[0]/10000,2)}", 55, 75,WIDTH,2)
144 | display.text(f" Used : {round(diskusage[1],2)}", 55, 90,WIDTH,2)
145 | display.text(f" Free : {round(diskusage[2],2)}", 55, 105,WIDTH,2)
146 |
147 | # Show the CPU frequency
148 | display.text(f" CPU", 205, 80,WIDTH,2)
149 | display.text(f" {cpu_freq} MHz", 205, 100,WIDTH,2)
150 |
151 | display.update()
152 |
153 |
154 | #utime.sleep_ms(2000)
155 | #badger_os.launch('launcher')
156 |
157 | # Call halt in a loop, on battery this switches off power.
158 | # On USB, the app will exit when A+C is pressed because the launcher picks that up.
159 | while True:
160 | display.keepalive()
161 | display.halt()
162 |
--------------------------------------------------------------------------------
/examples/qrgen.py:
--------------------------------------------------------------------------------
1 | import badger2040
2 | import qrcode
3 | import time
4 | import os
5 | import badger_os
6 |
7 | # Check that the qrcodes directory exists, if not, make it
8 | try:
9 | os.mkdir("/qrcodes")
10 | except OSError:
11 | pass
12 |
13 | # Check that there is a qrcode.txt, if not preload
14 | try:
15 | text = open("/qrcodes/qrcode.txt", "r")
16 | except OSError:
17 | text = open("/qrcodes/qrcode.txt", "w")
18 | if badger2040.is_wireless():
19 | text.write("""https://pimoroni.com/badger2040w
20 | Badger 2040 W
21 | * 296x128 1-bit e-ink
22 | * 2.4GHz wireless & RTC
23 | * five user buttons
24 | * user LED
25 | * 2MB QSPI flash
26 |
27 | Scan this code to learn
28 | more about Badger 2040 W.
29 | """)
30 | else:
31 | text.write("""https://pimoroni.com/badger2040
32 | Badger 2040
33 | * 296x128 1-bit e-ink
34 | * five user buttons
35 | * user LED
36 | * 2MB QSPI flash
37 |
38 | Scan this code to learn
39 | more about Badger 2040.
40 | """)
41 | text.flush()
42 | text.seek(0)
43 |
44 | # Load all available QR Code Files
45 | try:
46 | CODES = [f for f in os.listdir("/qrcodes") if f.endswith(".txt")]
47 | TOTAL_CODES = len(CODES)
48 | except OSError:
49 | pass
50 |
51 |
52 | print(f'There are {TOTAL_CODES} QR Codes available:')
53 | for codename in CODES:
54 | print(f'File: {codename}')
55 |
56 | display = badger2040.Badger2040()
57 |
58 | code = qrcode.QRCode()
59 |
60 | state = {
61 | "current_qr": 0
62 | }
63 |
64 |
65 | def measure_qr_code(size, code):
66 | w, h = code.get_size()
67 | module_size = int(size / w)
68 | return module_size * w, module_size
69 |
70 |
71 | def draw_qr_code(ox, oy, size, code):
72 | size, module_size = measure_qr_code(size, code)
73 | display.set_pen(15)
74 | display.rectangle(ox, oy, size, size)
75 | display.set_pen(0)
76 | for x in range(size):
77 | for y in range(size):
78 | if code.get_module(x, y):
79 | display.rectangle(ox + x * module_size, oy + y * module_size, module_size, module_size)
80 |
81 |
82 | def draw_qr_file(n):
83 | display.led(128)
84 | file = CODES[n]
85 | codetext = open("/qrcodes/{}".format(file), "r")
86 |
87 | lines = codetext.read().strip().split("\n")
88 | code_text = lines.pop(0)
89 | title_text = lines.pop(0)
90 | detail_text = lines
91 |
92 | # Clear the Display
93 | display.set_pen(15) # Change this to 0 if a white background is used
94 | display.clear()
95 | display.set_pen(0)
96 |
97 | code.set_text(code_text)
98 | size, _ = measure_qr_code(128, code)
99 | left = top = int((badger2040.HEIGHT / 2) - (size / 2))
100 | draw_qr_code(left, top, 128, code)
101 |
102 | left = 128 + 5
103 |
104 | display.text(title_text, left, 20, badger2040.WIDTH, 2)
105 |
106 | top = 40
107 | for line in detail_text:
108 | display.text(line, left, top, badger2040.WIDTH, 1)
109 | top += 10
110 |
111 | if TOTAL_CODES > 1:
112 | for i in range(TOTAL_CODES):
113 | x = 286
114 | y = int((128 / 2) - (TOTAL_CODES * 10 / 2) + (i * 10))
115 | display.set_pen(0)
116 | display.rectangle(x, y, 8, 8)
117 | if state["current_qr"] != i:
118 | display.set_pen(15)
119 | display.rectangle(x + 1, y + 1, 6, 6)
120 | display.update()
121 |
122 |
123 | badger_os.state_load("qrcodes", state)
124 | changed = True
125 |
126 | while True:
127 | # Sometimes a button press or hold will keep the system
128 | # powered *through* HALT, so latch the power back on.
129 | display.keepalive()
130 |
131 | if TOTAL_CODES > 1:
132 | if display.pressed(badger2040.BUTTON_UP):
133 | if state["current_qr"] > 0:
134 | state["current_qr"] -= 1
135 | changed = True
136 |
137 | if display.pressed(badger2040.BUTTON_DOWN):
138 | if state["current_qr"] < TOTAL_CODES - 1:
139 | state["current_qr"] += 1
140 | changed = True
141 |
142 | if display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C):
143 | display.set_pen(15)
144 | display.clear()
145 | badger_os.warning(display, "To add QR codes, connect Badger 2040 W to a PC, load up Thonny, and add files to /qrcodes directory.")
146 | time.sleep(4)
147 | changed = True
148 |
149 | if changed:
150 | draw_qr_file(state["current_qr"])
151 | badger_os.state_save("qrcodes", state)
152 | changed = False
153 |
154 | # Halt the Badger to save power, it will wake up if any of the front buttons are pressed
155 | display.halt()
156 |
--------------------------------------------------------------------------------
/examples/sendODK.py:
--------------------------------------------------------------------------------
1 | import urequests
2 | import binascii
3 | import network
4 | import os
5 | import uos
6 | import time
7 | import badger2040
8 |
9 | # Initialize the Badger eINK display
10 | display = badger2040.Badger2040()
11 | display.led(128)
12 | display.set_update_speed(2)
13 |
14 | # Constants
15 | WIDTH = 250 # Width of the Badger2040 display
16 | HEIGHT = 122 # Height of the Badger2040 display
17 |
18 | # Your existing functions (connect_to_wifi, base64_encode, submit_submission, log_submission, walk, load_submitted_files, submit_xml_files_in_folder) go here...
19 |
20 | def connect_to_wifi(ssid, password):
21 | # set up the screen with a black background and black pen
22 | display.set_pen(0) # Change this to 0 if a white background is used
23 | display.clear()
24 | display.set_pen(15)
25 |
26 | wlan = network.WLAN(network.STA_IF)
27 | wlan.active(True)
28 |
29 | if wlan.isconnected():
30 | display.text('Already connected to Wi-Fi', 10, 20)
31 | display.update()
32 | return
33 |
34 | wlan.connect(ssid, password)
35 |
36 | while not wlan.isconnected():
37 | display.text('Connecting to Wi-Fi...', 10, 20)
38 | display.update()
39 |
40 | display.text('Connected to Wi-Fi', 10, 40)
41 | display.text('Network config:' + str(wlan.ifconfig()), 10, 60)
42 | display.update()
43 |
44 | def base64_encode(string):
45 | # Encoding the string to bytes
46 | encoded_bytes = string.encode('utf-8')
47 |
48 | # Encoding bytes to base64
49 | encoded_base64_bytes = binascii.b2a_base64(encoded_bytes)
50 |
51 | # Decoding base64 bytes to string
52 | encoded_base64_string = encoded_base64_bytes.decode('utf-8').strip()
53 |
54 | return encoded_base64_string
55 |
56 | def submit_submission(file_path, url, username, password):
57 | with open(file_path, 'r') as file:
58 | xml_data = file.read()
59 |
60 | encoded_credentials = base64_encode(username + ':' + password)
61 |
62 | headers = {
63 | 'Content-Type': 'application/xml',
64 | 'Authorization': 'Basic ' + encoded_credentials
65 | }
66 |
67 | response = urequests.post(url, headers=headers, data=xml_data)
68 |
69 | if response.status_code in (200, 201):
70 | display.set_pen(0) # Change this to 0 if a white background is used
71 | display.clear()
72 | display.set_pen(15)
73 | display.text('Submission successful', 10, 60)
74 | display.update()
75 | log_submission(file_path, success=True, response_text=response.text)
76 | else:
77 | display.set_pen(0) # Change this to 0 if a white background is used
78 | display.clear()
79 | display.set_pen(15)
80 | display.text('Submission failed: ' + str(response.status_code), 10, 40)
81 | display.text('Response: ' + response.text, 10, 60)
82 | display.update()
83 | log_submission(file_path, success=False, response_text=response.text)
84 |
85 | def log_submission(file_path, success, response_text):
86 | with open('log.txt', 'a') as log_file:
87 | status = 'Success' if success else 'Failure'
88 | log_entry = f"File: {file_path}, Status: {status}, Response: {response_text}\n"
89 | log_file.write(log_entry)
90 |
91 |
92 | def walk(directory):
93 | file_paths = []
94 | for entry in uos.listdir(directory):
95 | entry_path = directory + '/' + entry
96 | if uos.stat(entry_path)[0] & 0x4000:
97 | file_paths.extend(walk(entry_path))
98 | else:
99 | file_paths.append(entry_path)
100 | return file_paths
101 |
102 |
103 | def load_submitted_files():
104 | submitted_files = set()
105 | if 'log.txt' in uos.listdir():
106 | print("Log file exists")
107 | with open('log.txt', 'r') as log_file:
108 | for line in log_file:
109 | file_path, status, _ = line.strip().split(', ')
110 | if status == 'Status: Success': # Correct the status check
111 | # Extract the filename from the full file path
112 | filename = file_path.split('/')[-1]
113 | print("Adding filename to submitted files:", filename)
114 | submitted_files.add(filename)
115 | else:
116 | print("Skipping file:", file_path, "with status:", status)
117 | else:
118 | print("Log file does not exist")
119 | print("Submitted files:", submitted_files)
120 | return submitted_files
121 |
122 | def count_unique_uuids(log_file_path):
123 | unique_uuids = set()
124 | if log_file_path in uos.listdir():
125 | with open(log_file_path, 'r') as log_file:
126 | for line in log_file:
127 | file_path, _, response = line.strip().split(', ')
128 | uuid = response.split('/')[-1]
129 | unique_uuids.add(uuid)
130 | return len(unique_uuids)
131 |
132 | def submit_xml_files_in_folder(folder_path, url, username, password):
133 | # Load the set of submitted files from the log
134 | submitted_files = load_submitted_files()
135 |
136 | # Add a counter for successful submissions
137 | successful_submissions = 0
138 |
139 | file_paths = walk(folder_path)
140 | for file_path in file_paths:
141 | if file_path.endswith('.xml'):
142 | # Extract the filename from the full file path
143 | filename = file_path.split('/')[-1]
144 | if filename not in submitted_files:
145 | print('Submitting file:', file_path)
146 |
147 | # Instead of printing, use the Badger eINK display to show the submission status.
148 | display.set_pen(0) # Change this to 0 if a white background is used
149 | display.clear()
150 | display.set_pen(15)
151 | display.text('Submitting file:', 10, 40)
152 | display.text(file_path, 10, 60)
153 | display.update()
154 |
155 | submit_submission(file_path, url, username, password)
156 | print('---')
157 |
158 | # Update the log and counter only after a successful submission
159 | if filename not in submitted_files:
160 | log_submission(file_path, success=True, response_text='')
161 | successful_submissions += 1
162 |
163 | submitted_files.add(filename) # Update the set with the filename
164 |
165 | # Get the total number of unique UUID numbers in the log file
166 | unique_uuid_count = count_unique_uuids('log.txt')
167 |
168 | # Display the total number of successful submissions
169 | display.set_pen(0) # Change this to 0 if a white background is used
170 | display.clear()
171 | display.set_pen(15)
172 |
173 | display.text('Submissions Sent Now:', 10, 20)
174 | display.text(str(successful_submissions), 10, 40)
175 | display.text('Total Submissions :', 10, 60)
176 | display.text(str(unique_uuid_count), 10, 80)
177 | display.update()
178 |
179 | print("Submitted files:", submitted_files)
180 |
181 |
182 | ##########################
183 | # MAIN
184 | ##########################
185 |
186 | print("Connecting to Wi-Fi...")
187 | connect_to_wifi('YOURNETWORKSSID', 'YOURNETWORKPASSWORD")
188 |
189 | folder_path = 'instances' # Update with the actual folder path
190 | url = 'https://YOURCENTRALURL/v1/projects/YOURPROJECTID/forms/YOURFORMNAME/submissions'
191 | username = 'YOURCENTRALUSERNAME"
192 | password = 'YOURCENTRALPASSWORD'
193 |
194 | submit_xml_files_in_folder(folder_path, url, username, password)
195 |
--------------------------------------------------------------------------------
/examples/space.py:
--------------------------------------------------------------------------------
1 | # This example grabs current weather details from Open Meteo and displays them on Badger 2040 W.
2 | # Find out more about the Open Meteo API at https://open-meteo.com
3 |
4 | import badger2040
5 | from badger2040 import WIDTH
6 | import urequests
7 | import jpegdec
8 | import machine
9 |
10 | rtc = machine.RTC()
11 |
12 | # Set display parameters
13 | WIDTH = badger2040.WIDTH
14 | HEIGHT = badger2040.HEIGHT
15 |
16 | # Set your latitude/longitude here (find yours by right clicking in Google Maps!)
17 | LAT = 63.38609085276884
18 | LNG = -1.4239983439328177
19 | TIMEZONE = "auto" # determines time zone from lat/long
20 |
21 | URL = "http://api.open-meteo.com/v1/forecast?latitude=" + str(LAT) + "&longitude=" + str(LNG) + "¤t_weather=true&daily=weathercode,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,precipitation_sum,precipitation_probability_max,winddirection_10m_dominant&timezone=" + TIMEZONE
22 |
23 | # Declare cleaned_lines as a global variable to store the extracted data
24 | cleaned_lines = []
25 |
26 | # Display Setup
27 | display = badger2040.Badger2040()
28 |
29 | display.led(128)
30 | display.set_update_speed(2)
31 |
32 | jpeg = jpegdec.JPEG(display.display)
33 |
34 |
35 |
36 | def get_data():
37 | global weathercode, temperature, windspeed, winddirection, date, time, day_weathercode, apparent_temperature_max, apparent_temperature_min, sunrise, sunset, precipitation_sum, precipitation_probability_max, winddirection_10m_dominant
38 | print(f"Requesting URL: {URL}")
39 | r = urequests.get(URL)
40 | # open the json data
41 | j = r.json()
42 | print("Data obtained!")
43 | print(j)
44 |
45 | # parse relevant data from JSON
46 | current = j["current_weather"]
47 | temperature = current["temperature"]
48 | windspeed = current["windspeed"]
49 | winddirection = calculate_bearing(current["winddirection"])
50 | weathercode = current["weathercode"]
51 | date, time = current["time"].split("T")
52 |
53 | daily = j["daily"]
54 | day_weathercode = daily["weathercode"]
55 | apparent_temperature_max = daily["apparent_temperature_max"]
56 | apparent_temperature_min = daily["apparent_temperature_min"]
57 | sunrise = daily["sunrise"]
58 | sunrise = sunrise[1]
59 | sunrise = sunrise.split("T")[1]
60 | sunset = daily["sunset"]
61 | sunset = sunset[1]
62 | sunset = sunset.split("T")[1]
63 |
64 |
65 | precipitation_sum = daily["precipitation_sum"]
66 | precipitation_probability_max = daily["precipitation_probability_max"]
67 | winddirection_10m_dominant = daily["winddirection_10m_dominant"]
68 | winddirection_10m_dominant = calculate_bearing(winddirection_10m_dominant[1])
69 | r.close()
70 |
71 |
72 | def get_solar_weather():
73 |
74 | global cleaned_lines # Use the global cleaned_lines variable to store the extracted data
75 |
76 | global source, updated, solarflux, aindex, kindex, kindexnt, xray, sunspots, heliumline, protonflux, electonflux, aurora, normalization, latdegree, solarwind, magneticfield, geomagfield, signalnoise,fof2,muffactor, muf
77 |
78 | solar_url = "https://www.hamqsl.com/solarxml.php"
79 |
80 | # Display Setup
81 | display = badger2040.Badger2040()
82 |
83 | display.led(128)
84 | display.set_update_speed(2)
85 |
86 | jpeg = jpegdec.JPEG(display.display)
87 |
88 | # Make a GET request to the URL using urequests
89 | response = urequests.get(solar_url)
90 |
91 | # Check if the request was successful
92 | if response.status_code == 200:
93 | # Get the content as a string
94 | xml_content = response.content.decode('utf-8')
95 |
96 | # Manually extract data
97 | source = extract_element(xml_content, "source")
98 | updated = extract_element(xml_content, "updated")
99 | solarflux = extract_element(xml_content, "solarflux")
100 | aindex = extract_element(xml_content, "aindex")
101 | kindex = extract_element(xml_content, "kindex")
102 | kindexnt = extract_element(xml_content, "kindexnt")
103 | xray = extract_element(xml_content, "xray")
104 | sunspots = extract_element(xml_content, "sunspots")
105 | heliumline = extract_element(xml_content, "heliumline")
106 | protonflux = extract_element(xml_content, "protonflux")
107 | electonflux = extract_element(xml_content, "electonflux")
108 | aurora = extract_element(xml_content, "aurora")
109 | normalization = extract_element(xml_content, "normalization")
110 | latdegree = extract_element(xml_content, "latdegree")
111 | solarwind = extract_element(xml_content, "solarwind")
112 | magneticfield = extract_element(xml_content, "magneticfield")
113 | geomagfield = extract_element(xml_content, "geomagfield")
114 | signalnoise = extract_element(xml_content, "signalnoise")
115 | fof2 = extract_element(xml_content, "fof2")
116 | muffactor = extract_element(xml_content, "muffactor")
117 | muf = extract_element(xml_content, "muf")
118 |
119 | print("source:", source)
120 | print("updated:", updated)
121 | print("solarflux:", solarflux)
122 | print("aindex:", aindex)
123 | print("kindex:", kindex)
124 | print("kindexnt:", kindexnt)
125 | print("xray:", xray)
126 | print("sunspots:", sunspots)
127 | print("heliumline:", heliumline)
128 | print("protonflux:", protonflux)
129 | print("electonflux:", electonflux)
130 | print("aurora:", aurora)
131 | print("normalization:", normalization)
132 | print("latdegree:", latdegree)
133 | print("solarwind:", solarwind)
134 | print("magneticfield:", magneticfield)
135 | print("geomagfield:", geomagfield)
136 | print("signalnoise:", signalnoise)
137 | print("fof2:", fof2)
138 | print("muffactor:", muffactor)
139 | print("muf:", muf)
140 |
141 |
142 | # Extract the contents of the calculatedconditions node
143 | start_index = xml_content.find("")
144 | end_index = xml_content.find("") + len("")
145 | calculated_conditions = xml_content[start_index:end_index]
146 |
147 | # Process the calculated_conditions data
148 | lines = calculated_conditions.split('\n')
149 | cleaned_lines = []
150 | for line in lines:
151 | if line.strip().startswith("', ' : ').replace('', '')
153 | cleaned_lines.append(line)
154 |
155 |
156 | for line in cleaned_lines:
157 | print(line)
158 |
159 |
160 | else:
161 | print("Error:", response.status_code)
162 |
163 | # define function to extract elements of xml data
164 | def extract_element(content, element_name):
165 | start_tag = f"<{element_name}>"
166 | end_tag = f"{element_name}>"
167 | start_index = content.find(start_tag)
168 | end_index = content.find(end_tag)
169 | if start_index != -1 and end_index != -1:
170 | element_value = content[start_index + len(start_tag):end_index]
171 | return element_value
172 | return None
173 |
174 | def calculate_bearing(d):
175 | # calculates a compass direction from the wind direction in degrees
176 | dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
177 | ix = round(d / (360. / len(dirs)))
178 | return dirs[ix % len(dirs)]
179 |
180 |
181 | def draw_page():
182 | # Clear the display
183 | global cleaned_lines # Use the global cleaned_lines variable to display the extracted data
184 | print(f"cleaned lines {cleaned_lines}")
185 | display.set_pen(15)
186 | display.clear()
187 | display.set_pen(0)
188 |
189 | # Draw box around display
190 | display.line(2,0,2,HEIGHT-1,2)
191 | display.line(WIDTH-1,0,WIDTH-1,HEIGHT-1,2)
192 | display.line(2,HEIGHT-1,WIDTH-1,HEIGHT-1,2)
193 |
194 | # Draw divider lines vertical
195 | display.line(105,10,105,HEIGHT,1)
196 | display.line(245,10,245,40,1)
197 | display.line(190,40,190,HEIGHT,1)
198 |
199 | # Draw divider lines horizontal
200 | display.line(190,40,WIDTH,40,1)
201 |
202 | # Draw the page header
203 | display.set_font("bitmap8")
204 | display.set_pen(0)
205 | display.rectangle(0, 0, WIDTH, 10)
206 | display.set_pen(15)
207 | display.text("Current Space Weather", 10, 1, WIDTH, 0.6) # parameters are left padding, top padding, width of screen area, font size
208 | display.set_pen(0)
209 |
210 | display.set_font("bitmap8")
211 |
212 | if temperature is not None:
213 | # Choose an appropriate icon based on the weather code
214 | # Weather codes from https://open-meteo.com/en/docs
215 | # Weather icons from https://fontawesome.com/
216 | if weathercode in [71, 73, 75, 77, 85, 86]: # codes for snow
217 | jpeg.open_file("/icons/icon-snow.jpg")
218 | elif weathercode in [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82]: # codes for rain
219 | jpeg.open_file("/icons/icon-rain.jpg")
220 | elif weathercode in [1, 2, 3, 45, 48]: # codes for cloud
221 | jpeg.open_file("/icons/icon-cloud.jpg")
222 | elif weathercode in [0]: # codes for sun
223 | jpeg.open_file("/icons/icon-sun.jpg")
224 | elif weathercode in [95, 96, 99]: # codes for storm
225 | jpeg.open_file("/icons/icon-storm.jpg")
226 | jpeg.decode(260,5, jpegdec.JPEG_SCALE_HALF)
227 |
228 | # show current temperature, with highs and lows
229 | display.set_pen(0)
230 | display.text(f"{temperature}°C ", 110, 95, WIDTH - 50, 2)
231 | display.text(f"{apparent_temperature_min[1]}°C, {apparent_temperature_max[1]}°C", 110, 115, WIDTH - 50, 1)
232 |
233 | # Display each line with incremented horizontal position
234 | x_position = 190 # Initial x position
235 | y_position = 45 # y position for displaying cleaned lines
236 | for line in cleaned_lines:
237 | display.text(line, x_position, y_position, WIDTH, 0.6)
238 | y_position += 10 # Increment the y position for the next line
239 |
240 |
241 |
242 | # display solar weather data
243 |
244 | display.text(f"Solar Flux : {solarflux}", 10, 15, WIDTH - 105, 1.5)
245 | display.text(f"A index : {aindex}", 110, 45, WIDTH - 105, 1.5)
246 | display.text(f"K index : {kindex}", 110, 35, WIDTH - 105, 1.5)
247 | display.text(f"X-ray : {xray}", 10, 55, WIDTH - 105, 1.5)
248 | display.text(f"Sunspots : {sunspots}", 10, 25, WIDTH - 105, 1.5)
249 | display.text(f"Helium Line : {heliumline}", 10, 65, WIDTH - 105, 1.5)
250 | display.text(f"Proton Flux : {protonflux}", 10, 75, WIDTH - 105, 1.5)
251 | display.text(f"Electron Flux : {electonflux}", 10, 85, WIDTH - 105, 1.5)
252 | display.text(f"Aurora : {aurora}", 10, 95, WIDTH - 105, 1.5)
253 | display.text(f"Normalisation : {normalization}", 10, 105, WIDTH - 105, 1.5)
254 | display.text(f"Lat Degree : {latdegree}", 10, 115, WIDTH - 105, 1.5)
255 |
256 | display.text(f"Magnetic Field : {magneticfield}", 110, 25, WIDTH - 105, 1.5)
257 | display.text(f"Solar Wind : {solarwind}", 10, 35, WIDTH - 105, 1.5)
258 | display.text(f"Geomagnetic Field : {geomagfield}", 110, 15, WIDTH - 105, 1.5)
259 | display.text(f"Signal Noise : {signalnoise}", 10, 45, WIDTH - 105, 1.5)
260 | display.text(f"FOF-2 : {fof2}", 110, 55, WIDTH - 105, 1.5)
261 | display.text(f"MUF Factor : {muffactor}", 110, 65, WIDTH - 105, 1.5)
262 | display.text(f"MUF : {muf}", 110, 75, WIDTH - 105, 1.5)
263 |
264 | # show date and time
265 |
266 | display.set_pen(15)
267 | # display.text(f"{date}", 120,1 , WIDTH - 105, 1)
268 | display.text(f"{updated}", 120,1 , WIDTH - 105, 1)
269 |
270 | else:
271 | display.set_pen(0)
272 | display.rectangle(0, 60, WIDTH, 25)
273 | display.set_pen(15)
274 | display.text("Unable to display weather! Check your network settings in WIFI_CONFIG.py", 5, 65, WIDTH, 1)
275 |
276 | display.update()
277 |
278 | # Connects to the wireless network. Ensure you have entered your details in WIFI_CONFIG.py :).
279 | print("connecting")
280 | display.connect()
281 |
282 | get_data()
283 | get_solar_weather()
284 | draw_page()
285 |
286 | # Call halt in a loop, on battery this switches off power.
287 | # On USB, the app will exit when A+C is pressed because the launcher picks that up.
288 | while True:
289 | display.keepalive()
290 | display.halt()
291 |
292 |
293 |
--------------------------------------------------------------------------------
/examples/totp.py:
--------------------------------------------------------------------------------
1 | import time
2 | import machine
3 | import utime
4 | import ntptime
5 | import struct
6 | import badger2040
7 | import badger_os
8 | import ujson as json
9 | import network
10 | from pcf85063a import PCF85063A
11 |
12 |
13 |
14 | badger = badger2040.Badger2040()
15 | badger.connect()
16 | badger.set_font("bitmap16")
17 | badger.set_update_speed(2)
18 |
19 | # Set display parameters
20 | WIDTH = badger2040.WIDTH
21 | HEIGHT = badger2040.HEIGHT
22 |
23 | if badger.isconnected():
24 | # Synchronize with the NTP server to get the current time
25 | print("Connected to Wi-Fi, setting time on RTC")
26 | ntptime.settime()
27 | print ("Disconnecting")
28 | wlan = network.WLAN()
29 | wlan.disconnect()
30 | print("Disconnected from Wi-Fi")
31 | else:
32 | print("No Wi-Fi")
33 |
34 | #set timezone offset
35 | timezone_offset = 0
36 |
37 | # Define SHA1 constants and utility functions
38 | HASH_CONSTANTS = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]
39 |
40 | # Set the time on the external PCF85063A RTC
41 | print("setting pcf time")
42 |
43 | now = utime.localtime()
44 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
45 | rtc_pcf85063a = PCF85063A(i2c)
46 | rtc_pcf85063a.datetime(now)
47 |
48 | # Set the time on the external PCF85063A RTC
49 | print("pcf time set")
50 |
51 |
52 | #####################################################
53 | # Define functions
54 | #####################################################
55 |
56 | def left_rotate(n, b):
57 | return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF
58 |
59 | def expand_chunk(chunk):
60 | w = list(struct.unpack(">16L", chunk)) + [0] * 64
61 | for i in range(16, 80):
62 | w[i] = left_rotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1)
63 | return w
64 |
65 | def sha1(message):
66 | h = HASH_CONSTANTS
67 | padded_message = message + b"\x80" + \
68 | (b"\x00" * (63 - (len(message) + 8) % 64)) + \
69 | struct.pack(">Q", 8 * len(message))
70 | chunks = [padded_message[i:i+64] for i in range(0, len(padded_message), 64)]
71 |
72 | for chunk in chunks:
73 | expanded_chunk = expand_chunk(chunk)
74 | a, b, c, d, e = h
75 | for i in range(0, 80):
76 | if 0 <= i < 20:
77 | f = (b & c) | ((~b) & d)
78 | k = 0x5A827999
79 | elif 20 <= i < 40:
80 | f = b ^ c ^ d
81 | k = 0x6ED9EBA1
82 | elif 40 <= i < 60:
83 | f = (b & c) | (b & d) | (c & d)
84 | k = 0x8F1BBCDC
85 | elif 60 <= i < 80:
86 | f = b ^ c ^ d
87 | k = 0xCA62C1D6
88 | a, b, c, d, e = (
89 | left_rotate(a, 5) + f + e + k + expanded_chunk[i] & 0xFFFFFFFF,
90 | a,
91 | left_rotate(b, 30),
92 | c,
93 | d,
94 | )
95 | h = (
96 | h[0] + a & 0xFFFFFFFF,
97 | h[1] + b & 0xFFFFFFFF,
98 | h[2] + c & 0xFFFFFFFF,
99 | h[3] + d & 0xFFFFFFFF,
100 | h[4] + e & 0xFFFFFFFF,
101 | )
102 |
103 | return struct.pack(">5I", *h)
104 |
105 | def hmac_sha1(key, message):
106 | key_block = key + (b'\0' * (64 - len(key)))
107 | key_inner = bytes((x ^ 0x36) for x in key_block)
108 | key_outer = bytes((x ^ 0x5C) for x in key_block)
109 |
110 | inner_message = key_inner + message
111 | outer_message = key_outer + sha1(inner_message)
112 |
113 | return sha1(outer_message)
114 |
115 | def base32_decode(message):
116 | padded_message = message + '=' * (8 - len(message) % 8)
117 | chunks = [padded_message[i:i+8] for i in range(0, len(padded_message), 8)]
118 |
119 | decoded = []
120 |
121 | for chunk in chunks:
122 | bits = 0
123 | bitbuff = 0
124 |
125 | for c in chunk:
126 | if 'A' <= c <= 'Z':
127 | n = ord(c) - ord('A')
128 | elif '2' <= c <= '7':
129 | n = ord(c) - ord('2') + 26
130 | elif c == '=':
131 | continue
132 | else:
133 | raise ValueError("Not Base32")
134 |
135 | bits += 5
136 | bitbuff <<= 5
137 | bitbuff |= n
138 |
139 | if bits >= 8:
140 | bits -= 8
141 | byte = bitbuff >> bits
142 | bitbuff &= ~(0xFF << bits)
143 | decoded.append(byte)
144 |
145 | return bytes(decoded)
146 |
147 | def totp(time, key, step_secs=30, digits=6):
148 | hmac = hmac_sha1(base32_decode(key), struct.pack(">Q", time // step_secs))
149 | offset = hmac[-1] & 0xF
150 | code = ((hmac[offset] & 0x7F) << 24 |
151 | (hmac[offset + 1] & 0xFF) << 16 |
152 | (hmac[offset + 2] & 0xFF) << 8 |
153 | (hmac[offset + 3] & 0xFF))
154 | code = str(code % 10 ** digits)
155 |
156 | # Add debugging prints
157 | # print(f"HMAC: {hmac.hex()}")
158 | # print(f"Offset: {offset}")
159 | # print(f"Code: {code}")
160 |
161 | return (
162 | "0" * (digits - len(code)) + code,
163 | step_secs - time % step_secs
164 | )
165 |
166 | #####################################################
167 | # Load keys from the JSON file
168 | #####################################################
169 |
170 | with open('data/totp_keys.json', 'r') as json_file:
171 | keys = json.load(json_file)
172 |
173 | # Display the current OTP codes once at startup
174 | key_info = []
175 | x = 10 # Initial x position
176 | y = 20 # Initial y position
177 |
178 | #####################################################
179 | # Get and check current times
180 | #####################################################
181 |
182 | def get_pcf_time():
183 | current_time = time.time()
184 | current_time_pcf = machine.RTC().datetime()
185 | print("current time system:", current_time)
186 | print("current time pcf:", current_time_pcf)
187 |
188 | # Extract the components from the tuple
189 | year, month, day, weekday, hour, minute, second, yearday = current_time_pcf
190 |
191 | # Convert the extracted components to integers
192 | year = int(year)
193 | month = int(month)
194 | day = int(day)
195 | hour = int(hour)
196 | minute = int(minute)
197 | second = int(second)
198 |
199 | # Calculate the Unix timestamp using time.mktime
200 | current_time_pcf = time.mktime((year, month, day, hour, minute, second, weekday, yearday))
201 |
202 | return current_time_pcf
203 |
204 | print(f"current time standard : {time.time()}")
205 | print(f"current time pfc : {get_pcf_time()}")
206 | print(f"time.time {time.time()}")
207 |
208 |
209 |
210 | for key in keys:
211 | name = key["name"]
212 | secret_key = key["key"]
213 | otp_value, sec_remain = totp(get_pcf_time(), secret_key, 30, 6)
214 |
215 | key_info.append(f"{otp_value} : {name}")
216 |
217 | badger.set_pen(15)
218 | badger.clear()
219 | # Draw the page header
220 | badger.set_font("bitmap8")
221 | badger.set_pen(15)
222 | badger.rectangle(0, 0, WIDTH, 10)
223 | badger.set_pen(0)
224 | badger.rectangle(0, 10, WIDTH, HEIGHT)
225 | badger.text("Badger TOTP Authenticator", 10, 1, WIDTH, 0.6)
226 | badger.text(f"Time to refresh : {sec_remain} S", 180, 1, WIDTH, 0.6)
227 |
228 | badger.set_pen(15)
229 |
230 | for info in key_info:
231 | badger.text(info, x, y, WIDTH, 0.6)
232 | y += 10
233 |
234 | # Check if y has reached HEIGHT - 15
235 | if y >= HEIGHT - 15:
236 | y = 20 # Reset y to its original value
237 | x += 100 # Add 80 to x
238 |
239 | #show current date and time
240 | spot_time = machine.RTC().datetime()
241 | year = spot_time[0]
242 | month = spot_time[1]
243 | day = spot_time[2]
244 | hour = spot_time[4]
245 | minute = spot_time[5]
246 | hour = hour + timezone_offset
247 |
248 | month = ('00' + str(month))[-2:]
249 | day = ('00' + str(day))[-2:]
250 | hour = ('00' + str(hour))[-2:]
251 | minute = ('00' + str(minute))[-2:]
252 | badger.text(f"{year}-{month}-{day}", 200, 70, WIDTH, 2)
253 | badger.text(f"{hour}:{minute}", 200, 90, WIDTH, 3)
254 | badger.update()
255 |
256 | #set variable for inversion of colours, aimed at stopping screen burn
257 | invert_colors = False
258 |
259 | while True:
260 | badger.keepalive()
261 |
262 | # Calculate the current OTP value and remaining time until next refresh
263 | null, cadence = otp_value, remaining = totp(get_pcf_time(), "LMESUJEY7PTJSNYO5LKSME5HWQO6XZ5L", 30, 6)
264 |
265 | if cadence == 30:
266 | # If the cadence timer is zero or negative, it's time to refresh
267 | invert_colors = not invert_colors # Toggle the color state
268 |
269 | key_info = []
270 | x = 10 # Initial x position
271 | y = 20 # Initial y position
272 |
273 | for key in keys:
274 | name = key["name"]
275 | secret_key = key["key"]
276 | otp_value, remaining = totp(get_pcf_time(), secret_key, 30, 6)
277 | key_info.append(f"{otp_value} : {name}")
278 | sec_remain = max(sec_remain, remaining)
279 | pen_color = 0 if invert_colors else 15
280 | pen_color_2 = 15 if invert_colors else 0
281 |
282 | badger.set_pen(pen_color)
283 | badger.clear()
284 | # Draw the page header
285 | badger.set_font("bitmap8")
286 | badger.set_pen(pen_color)
287 | badger.rectangle(0, 0, WIDTH, 10)
288 | badger.set_pen(pen_color_2)
289 | badger.rectangle(0, 10, WIDTH, HEIGHT)
290 | badger.text("Badger TOTP Authenticator", 10, 1, WIDTH, 0.6)
291 | badger.text(f"Time to refresh : {sec_remain} S", 180, 1, WIDTH, 0.6)
292 | print(f"Time to refresh : {sec_remain} S")
293 | badger.set_pen(pen_color)
294 |
295 | for info in key_info:
296 | badger.text(info, x, y, WIDTH, 0.6)
297 | y += 10
298 |
299 | # Check if y has reached HEIGHT - 15
300 | if y >= HEIGHT - 15:
301 | y = 20 # Reset y to its original value
302 | x += 100 # Add 80 to x
303 | #show current date and time
304 | #show current date and time
305 | #show current date and time
306 | spot_time = machine.RTC().datetime()
307 | year = spot_time[0]
308 | month = spot_time[1]
309 | day = spot_time[2]
310 | hour = spot_time[4]
311 | minute = spot_time[5]
312 | hour = hour + timezone_offset
313 |
314 | month = ('00' + str(month))[-2:]
315 | day = ('00' + str(day))[-2:]
316 | hour = ('00' + str(hour))[-2:]
317 | minute = ('00' + str(minute))[-2:]
318 | badger.text(f"{year}-{month}-{day}", 200, 70, WIDTH, 2)
319 | badger.text(f"{hour}:{minute}", 200, 90, WIDTH, 3)
320 | badger.update()
321 | utime.sleep_ms(25000)
322 | null, cadence = otp_value, remaining = totp(get_pcf_time(), "LMESUJEY7PTJSNYO5LKSME5HWQO6XZ5L", 30, 6)
323 | # Put the microcontroller into deep sleep during cadence countdown
324 | # Sleep for 30 seconds (cadence duration)
325 | if cadence > 0:
326 | cadence -= 1
327 | # Sleep in milliseconds
328 |
329 |
330 |
331 |
332 |
--------------------------------------------------------------------------------
/examples/totp2.py:
--------------------------------------------------------------------------------
1 | # v2.01
2 | # *After setting clock, the Wifi is disconnected AND the WLAN is shut down to save energy when running on battery.
3 | # *The polling loop now has a utime.sleep(5) to reduce the CPU usage and save battery. To actuate the refresh, hold button A for up to 5 seconds
4 | #
5 | # v2.00
6 | # *Changes from automated refreshment of screen so that a putton push prompts update.
7 |
8 | import time
9 | import machine
10 | import utime
11 | import ntptime
12 | import struct
13 | import badger2040
14 | import badger_os
15 | import ujson as json
16 | import network
17 | from pcf85063a import PCF85063A
18 |
19 | # Initialize the Badger2040
20 | badger = badger2040.Badger2040()
21 | badger.connect()
22 | badger.set_font("bitmap16")
23 | badger.set_update_speed(2)
24 |
25 | # Set display parameters
26 | WIDTH = badger2040.WIDTH
27 | HEIGHT = badger2040.HEIGHT
28 |
29 | if badger.isconnected():
30 | # Synchronize with the NTP server to get the current time
31 | print("Connected to Wi-Fi, setting time on RTC")
32 | ntptime.settime()
33 |
34 | # Disconnect and power down Wi-Fi
35 | wlan = network.WLAN(network.STA_IF)
36 | wlan.disconnect()
37 | wlan.active(False)
38 | print("Disconnected and Wi-Fi powered down")
39 | else:
40 | print("No Wi-Fi")
41 |
42 | # Set timezone offset
43 | timezone_offset = 1
44 |
45 | # Set variable for inversion of colours, aimed at stopping screen burn
46 | invert_colors = False
47 | pen_color = 15
48 | pen_color_2 = 0
49 |
50 | # Define SHA1 constants and utility functions
51 | HASH_CONSTANTS = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]
52 |
53 | # Set the time on the external PCF85063A RTC
54 | print("Setting PCF time")
55 |
56 | now = utime.localtime()
57 | i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4))
58 | rtc_pcf85063a = PCF85063A(i2c)
59 | rtc_pcf85063a.datetime(now)
60 |
61 | print("PCF time set")
62 |
63 | # Define functions
64 | def left_rotate(n, b):
65 | return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF
66 |
67 | def expand_chunk(chunk):
68 | w = list(struct.unpack(">16L", chunk)) + [0] * 64
69 | for i in range(16, 80):
70 | w[i] = left_rotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1)
71 | return w
72 |
73 | def sha1(message):
74 | h = HASH_CONSTANTS
75 | padded_message = message + b"\x80" + \
76 | (b"\x00" * (63 - (len(message) + 8) % 64)) + \
77 | struct.pack(">Q", 8 * len(message))
78 | chunks = [padded_message[i:i+64] for i in range(0, len(padded_message), 64)]
79 |
80 | for chunk in chunks:
81 | expanded_chunk = expand_chunk(chunk)
82 | a, b, c, d, e = h
83 | for i in range(0, 80):
84 | if 0 <= i < 20:
85 | f = (b & c) | ((~b) & d)
86 | k = 0x5A827999
87 | elif 20 <= i < 40:
88 | f = b ^ c ^ d
89 | k = 0x6ED9EBA1
90 | elif 40 <= i < 60:
91 | f = (b & c) | (b & d) | (c & d)
92 | k = 0x8F1BBCDC
93 | else: # 60 <= i < 80
94 | f = b ^ c ^ d
95 | k = 0xCA62C1D6
96 | a, b, c, d, e = (
97 | (left_rotate(a, 5) + f + e + k + expanded_chunk[i]) & 0xFFFFFFFF,
98 | a,
99 | left_rotate(b, 30),
100 | c,
101 | d,
102 | )
103 | h = (
104 | (h[0] + a) & 0xFFFFFFFF,
105 | (h[1] + b) & 0xFFFFFFFF,
106 | (h[2] + c) & 0xFFFFFFFF,
107 | (h[3] + d) & 0xFFFFFFFF,
108 | (h[4] + e) & 0xFFFFFFFF,
109 | )
110 |
111 | return struct.pack(">5I", *h)
112 |
113 | def hmac_sha1(key, message):
114 | key_block = key + (b'\0' * (64 - len(key)))
115 | key_inner = bytes((x ^ 0x36) for x in key_block)
116 | key_outer = bytes((x ^ 0x5C) for x in key_block)
117 |
118 | inner_message = key_inner + message
119 | outer_message = key_outer + sha1(inner_message)
120 |
121 | return sha1(outer_message)
122 |
123 | def base32_decode(message):
124 | padded_message = message + '=' * (8 - len(message) % 8)
125 | chunks = [padded_message[i:i+8] for i in range(0, len(padded_message), 8)]
126 |
127 | decoded = []
128 |
129 | for chunk in chunks:
130 | bits = 0
131 | bitbuff = 0
132 |
133 | for c in chunk:
134 | if 'A' <= c <= 'Z':
135 | n = ord(c) - ord('A')
136 | elif '2' <= c <= '7':
137 | n = ord(c) - ord('2') + 26
138 | elif c == '=':
139 | continue
140 | else:
141 | raise ValueError("Not Base32")
142 |
143 | bits += 5
144 | bitbuff <<= 5
145 | bitbuff |= n
146 |
147 | if bits >= 8:
148 | bits -= 8
149 | byte = bitbuff >> bits
150 | bitbuff &= ~(0xFF << bits)
151 | decoded.append(byte)
152 |
153 | return bytes(decoded)
154 |
155 | def totp(time, key, step_secs=30, digits=6):
156 | hmac = hmac_sha1(base32_decode(key), struct.pack(">Q", time // step_secs))
157 | offset = hmac[-1] & 0xF
158 | code = ((hmac[offset] & 0x7F) << 24 |
159 | (hmac[offset + 1] & 0xFF) << 16 |
160 | (hmac[offset + 2] & 0xFF) << 8 |
161 | (hmac[offset + 3] & 0xFF))
162 | code = str(code % 10 ** digits)
163 |
164 | return (
165 | "0" * (digits - len(code)) + code,
166 | step_secs - time % step_secs
167 | )
168 |
169 | # Load keys from the JSON file
170 | with open('data/totp_keys.json', 'r') as json_file:
171 | keys = json.load(json_file)
172 |
173 | def get_pcf_time():
174 | current_time = time.time()
175 | current_time_pcf = machine.RTC().datetime()
176 | print("current time system:", current_time)
177 | print("current time pcf:", current_time_pcf)
178 |
179 | # Extract the components from the tuple
180 | year, month, day, weekday, hour, minute, second, yearday = current_time_pcf
181 |
182 | # Convert the extracted components to integers
183 | year = int(year)
184 | month = int(month)
185 | day = int(day)
186 | hour = int(hour)
187 | minute = int(minute)
188 | second = int(second)
189 |
190 | # Calculate the Unix timestamp using time.mktime
191 | current_time_pcf = time.mktime((year, month, day, hour, minute, second, weekday, yearday))
192 |
193 | return current_time_pcf
194 |
195 | print(f"current time standard : {time.time()}")
196 | print(f"current time pfc : {get_pcf_time()}")
197 | print(f"time.time {time.time()}")
198 |
199 | def display_otp():
200 | key_info = []
201 | x = 10 # Initial x position
202 | y = 20 # Initial y position
203 |
204 | for key in keys:
205 | name = key["name"]
206 | secret_key = key["key"]
207 | otp_value, sec_remain = totp(get_pcf_time(), secret_key, 30, 6)
208 | key_info.append(f"{otp_value} : {name}")
209 |
210 | badger.set_pen(pen_color)
211 | badger.clear()
212 | # Draw the page header
213 | badger.set_font("bitmap8")
214 | badger.set_pen(pen_color)
215 | badger.rectangle(0, 0, WIDTH, 10)
216 | badger.set_pen(pen_color_2)
217 | badger.rectangle(0, 10, WIDTH, HEIGHT)
218 | badger.text("Badger TOTP Authenticator", 10, 1, WIDTH, 0.6)
219 | badger.text(f"Time to refresh : {sec_remain} S", 180, 1, WIDTH, 0.6)
220 |
221 | badger.set_pen(pen_color)
222 |
223 | for info in key_info:
224 | badger.text(info, x, y, WIDTH, 0.6)
225 | y += 10
226 |
227 | if y >= HEIGHT - 15:
228 | y = 20 # Reset y to its original value
229 | x += 100 # Add 80 to x
230 |
231 | # Show current date and time
232 | spot_time = machine.RTC().datetime()
233 | year = spot_time[0]
234 | month = spot_time[1]
235 | day = spot_time[2]
236 | hour = spot_time[4]
237 | minute = spot_time[5]
238 | hour = hour + timezone_offset
239 |
240 | month = ('00' + str(month))[-2:]
241 | day = ('00' + str(day))[-2:]
242 | hour = ('00' + str(hour))[-2:]
243 | minute = ('00' + str(minute))[-2:]
244 | badger.text(f"{year}-{month}-{day}", 200, 70, WIDTH, 2)
245 | badger.text(f"{hour}:{minute}", 200, 90, WIDTH, 3)
246 | badger.update()
247 |
248 | # Initial display
249 | display_otp()
250 |
251 | while True:
252 | # Check for button press
253 | if badger.pressed(badger2040.BUTTON_A): # Replace BUTTON_A with your desired button
254 | utime.sleep_ms(50) # Debounce delay
255 | if badger.pressed(badger2040.BUTTON_A): # Check again if button is still pressed
256 | display_otp()
257 | invert_colors = not invert_colors
258 | pen_color = 0 if invert_colors else 15
259 | pen_color_2 = 15 if invert_colors else 0 # Toggle the color state
260 |
261 | while badger.pressed(badger2040.BUTTON_A):
262 | utime.sleep_ms(10) # Wait for the button to be released
263 |
264 | # Reduce polling frequency to save power
265 | utime.sleep(5) # Increase the sleep time to reduce CPU usage
266 |
267 |
--------------------------------------------------------------------------------
/examples/weather.py:
--------------------------------------------------------------------------------
1 | # This example grabs current weather details from Open Meteo and displays them on Badger 2040 W.
2 | # Find out more about the Open Meteo API at https://open-meteo.com
3 | #
4 | #
5 | # V3.0 changes
6 | # Adds new method to prevent screenburn which inverts colours each time the screen refreshes
7 | import badger2040
8 | from badger2040 import WIDTH
9 | import urequests
10 | import jpegdec
11 | import machine
12 | import random
13 |
14 | rtc = machine.RTC()
15 |
16 | # Set your latitude/longitude here (find yours by right clicking in Google Maps!)
17 | LAT = 52.104
18 | LNG = -0.0227
19 | TIMEZONE = "auto" # determines time zone from lat/long
20 |
21 |
22 | URL = "https://api.open-meteo.com/v1/forecast?latitude=" + str(LAT) + "&longitude=" + str(LNG) + "¤t_weather=true&daily=weathercode,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,precipitation_sum,precipitation_probability_max,winddirection_10m_dominant&timezone=" + TIMEZONE
23 | URL2 = "https://air-quality-api.open-meteo.com/v1/air-quality?latitude=" + str(LAT) + "&longitude=" + str(LNG) + "&hourly=pm10,pm2_5,uv_index,alder_pollen,birch_pollen,grass_pollen,mugwort_pollen,olive_pollen,ragweed_pollen"
24 |
25 | # Define foreground and background variable for color mode
26 | fg = 15 # Start with normal colors
27 | bg = 0
28 |
29 | # Display Setup
30 | display = badger2040.Badger2040()
31 |
32 |
33 | display.led(128)
34 | display.set_update_speed(2)
35 |
36 | jpeg = jpegdec.JPEG(display.display)
37 |
38 |
39 |
40 | def get_data():
41 | global weathercode, temperature, windspeed, winddirection, date, time, day_weathercode, apparent_temperature_max, apparent_temperature_min, sunrise, sunset, precipitation_sum, precipitation_probability_max, winddirection_10m_dominant
42 | print(f"Requesting URL: {URL}")
43 | r = urequests.get(URL)
44 | # open the json data
45 | j = r.json()
46 | print("Data obtained!")
47 | print(j)
48 |
49 | # parse relevant data from JSON
50 | current = j["current_weather"]
51 | temperature = current["temperature"]
52 | windspeed = current["windspeed"]
53 | winddirection = calculate_bearing(current["winddirection"])
54 | weathercode = current["weathercode"]
55 | date, time = current["time"].split("T")
56 |
57 | daily = j["daily"]
58 | day_weathercode = daily["weathercode"]
59 | apparent_temperature_max = daily["apparent_temperature_max"]
60 | apparent_temperature_min = daily["apparent_temperature_min"]
61 | sunrise = daily["sunrise"]
62 | sunrise = sunrise[1]
63 | sunrise = sunrise.split("T")[1]
64 | sunset = daily["sunset"]
65 | sunset = sunset[1]
66 | sunset = sunset.split("T")[1]
67 |
68 |
69 | precipitation_sum = daily["precipitation_sum"]
70 | precipitation_probability_max = daily["precipitation_probability_max"]
71 | winddirection_10m_dominant = daily["winddirection_10m_dominant"]
72 | winddirection_10m_dominant = calculate_bearing(winddirection_10m_dominant[1])
73 | r.close()
74 |
75 | def get_data_airquality():
76 | global pm10, pm2_5, alder_pollen, uv_index, birch_pollen, grass_pollen, mugwort_pollen, olive_pollen, ragweed_pollen
77 |
78 | print(f"Requesting URL: {URL2}")
79 | r2 = urequests.get(URL2)
80 |
81 | # open the json data
82 | j2 = r2.json()
83 | print("Airquality Data obtained!")
84 | print(j2)
85 |
86 | # parse relevant data from json
87 | airquality = j2["hourly"]
88 | print("Air quality:", airquality)
89 |
90 | # If a key doesn't exist, it'll default to a list with a single None item
91 | pm10 = airquality.get("pm10", [None])[1]
92 | pm2_5 = airquality.get("pm2_5", [None])[1]
93 |
94 | # Ensure uv_index list has no None values before applying max
95 | uv_values = [val for val in airquality.get("uv_index", []) if val is not None]
96 | uv_index = max(uv_values) if uv_values else 'NA'
97 |
98 | alder_pollen = airquality.get("alder_pollen", [None])[1]
99 | birch_pollen = airquality.get("birch_pollen", [None])[1]
100 | grass_pollen = airquality.get("grass_pollen", [None])[1]
101 | mugwort_pollen = airquality.get("mugwort_pollen", [None])[1]
102 | olive_pollen = airquality.get("olive_pollen", [None])[1]
103 | ragweed_pollen = airquality.get("ragweed_pollen", [None])[1]
104 |
105 | print(f"{uv_index} UVIndex ")
106 |
107 | r2.close()
108 |
109 |
110 | def calculate_bearing(d):
111 | # calculates a compass direction from the wind direction in degrees
112 | dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
113 | ix = round(d / (360. / len(dirs)))
114 | return dirs[ix % len(dirs)]
115 |
116 |
117 | def draw_page(text_color, background_color):
118 |
119 | # Define the icon file names based on the text color for simplicity
120 | # Assuming white text (15) uses dark icons and black text (0) uses light icons
121 | icon_prefix = "_dark" if text_color == 15 else ""
122 | icon_snow = f"/icons/icon-snow{icon_prefix}.jpg"
123 | icon_rain = f"/icons/icon-rain{icon_prefix}.jpg"
124 | icon_cloud = f"/icons/icon-cloud{icon_prefix}.jpg"
125 | icon_sun = f"/icons/icon-sun{icon_prefix}.jpg"
126 | icon_storm = f"/icons/icon-storm{icon_prefix}.jpg"
127 |
128 | # Clear the display with the background color
129 | display.set_pen(background_color)
130 | display.clear()
131 |
132 | # Use the text color for drawing text and other elements
133 | display.set_pen(text_color)
134 | display.rectangle(0, 0, WIDTH, 10)
135 | display.set_pen(background_color)
136 | display.text("Weather @ The Moving Castle", 10, 1, WIDTH, 0.6) # parameters are left padding, top padding, width of screen area, font size
137 | display.set_pen(text_color)
138 |
139 | display.set_font("bitmap8")
140 |
141 | if temperature is not None:
142 | # Choose an appropriate icon based on the weather code
143 | # Weather codes from https://open-meteo.com/en/docs
144 | # Weather icons from https://fontawesome.com/
145 | if weathercode in [71, 73, 75, 77, 85, 86]: # codes for snow
146 | jpeg.open_file(icon_snow)
147 | elif weathercode in [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82]: # codes for rain
148 | jpeg.open_file(icon_rain)
149 | elif weathercode in [1, 2, 3, 45, 48]: # codes for cloud
150 | jpeg.open_file(icon_cloud)
151 | elif weathercode in [0]: # codes for sun
152 | jpeg.open_file(icon_sun)
153 | elif weathercode in [95, 96, 99]: # codes for storm
154 | jpeg.open_file(icon_storm)
155 |
156 | try:
157 | jpeg.decode(10,30, jpegdec.JPEG_SCALE_FULL)
158 | except Exception as e:
159 | print("Error opening or decoding JPEG:", e)
160 |
161 | # show current temperature, with highs and lows
162 | display.set_pen(text_color)
163 | display.text(f"{temperature}°C ", 20, 95, WIDTH - 50, 2)
164 | display.text(f"{apparent_temperature_min[1]}°C, {apparent_temperature_max[1]}°C", 20, 115, WIDTH - 50, 1)
165 |
166 | # show prob and amount of rain today
167 | jpeg.open_file(icon_rain)
168 | jpeg.decode(100,20, jpegdec.JPEG_SCALE_HALF)
169 | display.set_pen(text_color)
170 | display.text(f"{precipitation_probability_max[1]}% ", 135, 25, WIDTH - 105, 2)
171 | display.text(f"{precipitation_sum[1]} mm ", 135, 45, WIDTH - 105, 1)
172 |
173 |
174 |
175 | # [{apparent_temperature_min[1]}°C, {apparent_temperature_max[1]}°C]
176 | # show five day high temperatures
177 | display.set_pen(text_color)
178 | # display.text(f"Forecast: {apparent_temperature_max[2]}°C | {apparent_temperature_max[3]}°C | {apparent_temperature_max[4]}°C | {apparent_temperature_max[5]}°C | {apparent_temperature_max[6]}°C", 10, 30, WIDTH - 50, 1)
179 | # show sunrise, sunset
180 | display.text(f"Wind : {windspeed} km/h {winddirection} | Prevailing : {winddirection_10m_dominant}", 100, 60, WIDTH - 105, 1.5)
181 | display.text(f"Sunrise : {sunrise} | Sunset : {sunset}", 100, 70, WIDTH - 105, 1.5)
182 |
183 | # Show tomorrow's weather
184 | print("Daily weathercodes")
185 | print(day_weathercode)
186 | if day_weathercode[2] in [71, 73, 75, 77, 85, 86]: # codes for snow
187 | jpeg.open_file(icon_snow)
188 | elif day_weathercode[2] in [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82]: # codes for rain
189 | jpeg.open_file(icon_rain)
190 | elif day_weathercode[2] in [1, 2, 3, 45, 48]: # codes for cloud
191 | jpeg.open_file(icon_cloud)
192 | elif day_weathercode[2] in [0]: # codes for sun
193 | jpeg.open_file(icon_sun)
194 | elif day_weathercode[2] in [95, 96, 99]: # codes for storm
195 | jpeg.open_file(icon_storm)
196 | display.set_pen(text_color)
197 | display.text("+1 Day", 160, 110, WIDTH - 105, 1.5)
198 | jpeg.decode(190,90, jpegdec.JPEG_SCALE_HALF)
199 |
200 | # Show day after tomorrow's weather
201 |
202 | if day_weathercode[3] in [71, 73, 75, 77, 85, 86]: # codes for snow
203 | jpeg.open_file(icon_snow)
204 | elif day_weathercode[3] in [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82]: # codes for rain
205 | jpeg.open_file(icon_rain)
206 | elif day_weathercode[3] in [1, 2, 3, 45, 48]: # codes for cloud
207 | jpeg.open_file(icon_cloud)
208 | elif day_weathercode[3] in [0]: # codes for sun
209 | jpeg.open_file(icon_sun)
210 | elif day_weathercode[3] in [95, 96, 99]: # codes for storm
211 | jpeg.open_file(icon_storm)
212 | display.set_pen(text_color)
213 | display.text("+2 Day", 230, 110, WIDTH - 105, 1.5)
214 | jpeg.decode(260,90, jpegdec.JPEG_SCALE_HALF)
215 |
216 | # display.text(f"Wind Direction: {winddirection}", int(WIDTH / 3), 68, WIDTH - 105, 2)
217 | display.set_pen(text_color)
218 | display.text(f"Updated {time}", 100, 90, WIDTH - 105, 1)
219 | # display pollen counts & particulate
220 | display.text(f"PM10 : {pm10}", 170, 15, WIDTH - 105, 1.5)
221 | display.text(f"Alder : {alder_pollen}", 170, 25, WIDTH - 105, 1.5)
222 | display.text(f"Grass : {grass_pollen}", 170, 35, WIDTH - 105, 1.5)
223 | display.text(f"Ragweed : {ragweed_pollen}", 170, 45, WIDTH - 105, 1.5)
224 | display.text(f"PM2.5 : {pm2_5}", 230, 15, WIDTH - 105, 1.5)
225 | display.text(f"Birch : {birch_pollen}", 230, 25, WIDTH - 105, 1.5)
226 | display.text(f"Mugwort : {mugwort_pollen}", 230, 35, WIDTH - 105, 1.5)
227 |
228 | # show date
229 |
230 | display.text(f"{date}", 1, 15, WIDTH - 105, 2)
231 | # show UV index
232 | display.text(f"Max UV Index : {uv_index}", 100, 80, WIDTH - 105, 1)
233 | else:
234 | display.set_pen(text_color)
235 | display.rectangle(0, 60, WIDTH, 25)
236 | display.set_pen(background_color)
237 | display.text("Unable to display weather! Check your network settings in WIFI_CONFIG.py", 5, 65, WIDTH, 1)
238 |
239 | display.update()
240 |
241 | # Connects to the wireless network. Ensure you have entered your details in WIFI_CONFIG.py :).
242 | print("connecting")
243 | display.connect()
244 |
245 | #get_data()
246 | #get_data_airquality()
247 | #draw_page(0, 15)
248 | #print("UV")
249 | #print (uv_index)
250 |
251 | # Call halt in a loop, on battery this switches off power.
252 | # On USB, the app will exit when A+C is pressed because the launcher picks that up.
253 | while True:
254 |
255 | # Define dark mode and light mode
256 | actions = [
257 | lambda: draw_page(0, 15),
258 | lambda: draw_page(15, 0)
259 | ]
260 |
261 | # Randomly select and execute one of the functions
262 | #Define the sleep interval between refreshes
263 | sleep_time = 15
264 |
265 | # do one cycle with dark mode colours
266 | print("waking & printing dark mode")
267 | get_data()
268 | get_data_airquality()
269 | random.choice(actions)()
270 | print("sleeping")
271 | badger2040.sleep_for(sleep_time) # Or whatever duration you need
272 |
273 |
274 |
275 |
276 |
277 |
--------------------------------------------------------------------------------
/forms/Badger 2040 Test.odkbuild:
--------------------------------------------------------------------------------
1 | {"title":"Badger 2040 Test","controls":[{"name":"name","label":{"0":"What is your name?"},"hint":{},"defaultValue":"","readOnly":false,"required":false,"requiredText":{},"relevance":"","constraint":"","invalidText":{},"calculate":"","short":{},"image":{},"audio":{},"video":{},"bigimage":{},"guidance":{},"length":false,"metadata":{},"type":"inputText"},{"name":"age","label":{"0":"What is your age?"},"hint":{},"defaultValue":"","readOnly":false,"required":false,"requiredText":{},"relevance":"","constraint":"","invalidText":{},"calculate":"","short":{},"image":{},"audio":{},"video":{},"bigimage":{},"guidance":{},"range":{"min":"10","max":"100","minInclusive":true,"maxInclusive":false},"appearance":"Textbox","kind":"Integer","selectRange":{"min":"1","max":"10"},"selectStep":"1","sliderTicks":true,"metadata":{},"type":"inputNumeric"},{"name":"sex","label":{"0":"What is your sex?"},"hint":{},"defaultValue":"F","readOnly":false,"required":false,"requiredText":{},"relevance":"","constraint":"","invalidText":{},"calculate":"","short":{},"image":{},"audio":{},"video":{},"bigimage":{},"guidance":{},"options":[{"text":{"0":"Male"},"cascade":[],"val":"M"},{"text":{"0":"Female"},"cascade":[],"val":"F"},{"text":{"0":"Other"},"cascade":[],"val":"O"}],"cascading":false,"other":false,"appearance":"Default","metadata":{},"type":"inputSelectOne"},{"name":"ate","label":{"0":"What did you eat today?"},"hint":{},"defaultValue":"","readOnly":false,"required":true,"requiredText":{},"relevance":"","constraint":"","invalidText":{},"calculate":"","short":{},"image":{},"audio":{},"video":{},"bigimage":{},"guidance":{},"options":[{"text":{"0":"Apple"},"val":"a"},{"text":{"0":"Orange"},"cascade":[],"val":"o"},{"text":{"0":"Banana"},"cascade":[],"val":"b"},{"text":{"0":"Pear"},"cascade":[],"val":"p"},{"text":{"0":"Kiwifruit"},"cascade":[],"val":"k"},{"text":{"0":"Avacado"},"cascade":[],"val":"av"}],"other":false,"count":false,"appearance":"Default","metadata":{},"type":"inputSelectMany"},{"name":"mood","label":{"0":"How are you feeling?"},"hint":{},"defaultValue":"","readOnly":false,"required":true,"requiredText":{},"relevance":"","constraint":"","invalidText":{},"calculate":"","short":{},"image":{},"audio":{},"video":{},"bigimage":{},"guidance":{},"options":[{"text":{"0":"joyful.jpg"},"cascade":[],"val":"5"},{"text":{"0":"happy.jpg"},"cascade":[],"val":"4"},{"text":{"0":"neutral.jpg"},"cascade":[],"val":"3"},{"text":{"0":"sad.jpg"},"cascade":[],"val":"2"},{"text":{"0":"angry.jpg"},"cascade":[],"val":"1"}],"cascading":false,"other":false,"appearance":"Horizontal Layout","metadata":{},"type":"inputSelectOne"},{"name":"animals","label":{"0":"Which animals can swim?"},"hint":{},"defaultValue":"","readOnly":false,"required":false,"requiredText":{},"relevance":"","constraint":"","invalidText":{},"calculate":"","short":{},"image":{},"audio":{},"video":{},"bigimage":{},"guidance":{},"options":[{"text":{"0":"a.jpg"},"val":"a"},{"text":{"0":"b.jpg"},"cascade":[],"val":"b"},{"text":{"0":"c.jpg"},"cascade":[],"val":"c"},{"text":{"0":"d.jpg"},"cascade":[],"val":"d"}],"other":false,"count":false,"appearance":"Horizontal Layout","metadata":{},"type":"inputSelectMany"}],"metadata":{"version":2,"activeLanguages":{"0":"English","_counter":0,"_display":"0"},"optionsPresets":[],"htitle":null,"instance_name":"","public_key":"","submission_url":"","location_min_interval":"","location_max_age":""}}
--------------------------------------------------------------------------------
/icons/a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/a.jpg
--------------------------------------------------------------------------------
/icons/angry.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/angry.jpg
--------------------------------------------------------------------------------
/icons/b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/b.jpg
--------------------------------------------------------------------------------
/icons/c.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/c.jpg
--------------------------------------------------------------------------------
/icons/d.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/d.jpg
--------------------------------------------------------------------------------
/icons/happy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/happy.jpg
--------------------------------------------------------------------------------
/icons/icon-cloud.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-cloud.jpg
--------------------------------------------------------------------------------
/icons/icon-cloud_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-cloud_dark.jpg
--------------------------------------------------------------------------------
/icons/icon-rain.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-rain.jpg
--------------------------------------------------------------------------------
/icons/icon-rain_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-rain_dark.jpg
--------------------------------------------------------------------------------
/icons/icon-snow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-snow.jpg
--------------------------------------------------------------------------------
/icons/icon-snow_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-snow_dark.jpg
--------------------------------------------------------------------------------
/icons/icon-storm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-storm.jpg
--------------------------------------------------------------------------------
/icons/icon-storm_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-storm_dark.jpg
--------------------------------------------------------------------------------
/icons/icon-sun.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-sun.jpg
--------------------------------------------------------------------------------
/icons/icon-sun_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/icon-sun_dark.jpg
--------------------------------------------------------------------------------
/icons/joyful.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/joyful.jpg
--------------------------------------------------------------------------------
/icons/neutral.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/neutral.jpg
--------------------------------------------------------------------------------
/icons/sad.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/icons/sad.jpg
--------------------------------------------------------------------------------
/img/3d_print_case.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/3d_print_case.png
--------------------------------------------------------------------------------
/img/3d_print_case_2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/3d_print_case_2.jpeg
--------------------------------------------------------------------------------
/img/apps_provision_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/apps_provision_01.jpg
--------------------------------------------------------------------------------
/img/apps_provision_02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/apps_provision_02.jpg
--------------------------------------------------------------------------------
/img/authenticator.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/authenticator.jpg
--------------------------------------------------------------------------------
/img/barchart.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/barchart.jpg
--------------------------------------------------------------------------------
/img/clk1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/clk1.png
--------------------------------------------------------------------------------
/img/clk2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/clk2.png
--------------------------------------------------------------------------------
/img/dash.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/dash.jpeg
--------------------------------------------------------------------------------
/img/heatmap_matrix.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/heatmap_matrix.jpg
--------------------------------------------------------------------------------
/img/heatmap_summary.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/heatmap_summary.jpg
--------------------------------------------------------------------------------
/img/logger_1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/logger_1.jpeg
--------------------------------------------------------------------------------
/img/logger_2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/logger_2.jpeg
--------------------------------------------------------------------------------
/img/space.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/space.jpeg
--------------------------------------------------------------------------------
/img/weather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrissyhroberts/badger2040w_code/b854b734005937bdb618a5c88011bd77ada15397/img/weather.png
--------------------------------------------------------------------------------
/lib/ahtx0.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 | #
3 | # Copyright (c) 2020 Kattni Rembor for Adafruit Industries
4 | # Copyright (c) 2020 Andreas Bühl
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | """
24 |
25 | MicroPython driver for the AHT10 and AHT20 Humidity and Temperature Sensor
26 |
27 | Author(s): Andreas Bühl, Kattni Rembor
28 |
29 | """
30 |
31 | import utime
32 | from micropython import const
33 |
34 |
35 | class AHT10:
36 | """Interface library for AHT10/AHT20 temperature+humidity sensors"""
37 |
38 | AHTX0_I2CADDR_DEFAULT = const(0x38) # Default I2C address
39 | AHTX0_CMD_INITIALIZE = 0xE1 # Initialization command
40 | AHTX0_CMD_TRIGGER = const(0xAC) # Trigger reading command
41 | AHTX0_CMD_SOFTRESET = const(0xBA) # Soft reset command
42 | AHTX0_STATUS_BUSY = const(0x80) # Status bit for busy
43 | AHTX0_STATUS_CALIBRATED = const(0x08) # Status bit for calibrated
44 |
45 | def __init__(self, i2c, address=AHTX0_I2CADDR_DEFAULT):
46 | utime.sleep_ms(20) # 20ms delay to wake up
47 | self._i2c = i2c
48 | self._address = address
49 | self._buf = bytearray(6)
50 | self.reset()
51 | if not self.initialize():
52 | raise RuntimeError("Could not initialize")
53 | self._temp = None
54 | self._humidity = None
55 |
56 | def reset(self):
57 | """Perform a soft-reset of the AHT"""
58 | self._buf[0] = self.AHTX0_CMD_SOFTRESET
59 | self._i2c.writeto(self._address, self._buf[0:1])
60 | utime.sleep_ms(20) # 20ms delay to wake up
61 |
62 | def initialize(self):
63 | """Ask the sensor to self-initialize. Returns True on success, False otherwise"""
64 | self._buf[0] = self.AHTX0_CMD_INITIALIZE
65 | self._buf[1] = 0x08
66 | self._buf[2] = 0x00
67 | self._i2c.writeto(self._address, self._buf[0:3])
68 | self._wait_for_idle()
69 | if not self.status & self.AHTX0_STATUS_CALIBRATED:
70 | return False
71 | return True
72 |
73 | @property
74 | def status(self):
75 | """The status byte initially returned from the sensor, see datasheet for details"""
76 | self._read_to_buffer()
77 | return self._buf[0]
78 |
79 | @property
80 | def relative_humidity(self):
81 | """The measured relative humidity in percent."""
82 | self._perform_measurement()
83 | self._humidity = (
84 | (self._buf[1] << 12) | (self._buf[2] << 4) | (self._buf[3] >> 4)
85 | )
86 | self._humidity = (self._humidity * 100) / 0x100000
87 | return self._humidity
88 |
89 | @property
90 | def temperature(self):
91 | """The measured temperature in degrees Celcius."""
92 | self._perform_measurement()
93 | self._temp = ((self._buf[3] & 0xF) << 16) | (self._buf[4] << 8) | self._buf[5]
94 | self._temp = ((self._temp * 200.0) / 0x100000) - 50
95 | return self._temp
96 |
97 | def _read_to_buffer(self):
98 | """Read sensor data to buffer"""
99 | self._i2c.readfrom_into(self._address, self._buf)
100 |
101 | def _trigger_measurement(self):
102 | """Internal function for triggering the AHT to read temp/humidity"""
103 | self._buf[0] = self.AHTX0_CMD_TRIGGER
104 | self._buf[1] = 0x33
105 | self._buf[2] = 0x00
106 | self._i2c.writeto(self._address, self._buf[0:3])
107 |
108 | def _wait_for_idle(self):
109 | """Wait until sensor can receive a new command"""
110 | while self.status & self.AHTX0_STATUS_BUSY:
111 | utime.sleep_ms(5)
112 |
113 | def _perform_measurement(self):
114 | """Trigger measurement and write result to buffer"""
115 | self._trigger_measurement()
116 | self._wait_for_idle()
117 | self._read_to_buffer()
118 |
119 |
120 | class AHT20(AHT10):
121 | AHTX0_CMD_INITIALIZE = 0xBE # Calibration command
122 |
--------------------------------------------------------------------------------
/provisioning_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "folders_to_clean": ["examples", "icons","data"],
3 | "files": [
4 | { "path": "examples/apps.py", "folder": "examples"},
5 | { "path": "examples/icon-apps.jpg", "folder": "examples"},
6 | { "path": "examples/logger.py", "folder": "examples"},
7 | { "path": "examples/icon-logger.jpg", "folder": "examples"},
8 | { "path": "examples/weather.py", "folder": "examples"},
9 | { "path": "examples/icon-weather.jpg", "folder": "examples"},
10 | { "path": "examples/space.py", "folder": "examples"},
11 | { "path": "examples/icon-space.jpg", "folder": "examples"},
12 | { "path": "examples/power.py", "folder": "examples"},
13 | { "path": "examples/icon-power.jpg", "folder": "examples"},
14 | { "path": "examples/totp2.py", "folder": "examples"},
15 | { "path": "examples/icon-totp2.jpg", "folder": "examples"},
16 | { "path": "examples/form.py", "folder": "examples"},
17 | { "path": "examples/icon-form.jpg", "folder": "examples"},
18 | { "path": "examples/sendODK.py", "folder": "examples"},
19 | { "path": "examples/icon-sendODK.jpg", "folder": "examples"},
20 | { "path": "data/data.csv", "folder": "data"},
21 | { "path": "data/totp_keys.json", "folder": "data"},
22 | { "path": "lib/ahtx0.py", "folder": "lib"},
23 | { "path": "icons/a.jpg", "folder": "icons"},
24 | { "path": "icons/b.jpg", "folder": "icons"},
25 | { "path": "icons/c.jpg", "folder": "icons"},
26 | { "path": "icons/d.jpg", "folder": "icons"},
27 | { "path": "icons/happy.jpg", "folder": "icons"},
28 | { "path": "icons/joyful.jpg", "folder": "icons"},
29 | { "path": "icons/neutral.jpg", "folder": "icons"},
30 | { "path": "icons/sad.jpg", "folder": "icons"},
31 | { "path": "icons/icon-sun.jpg", "folder": "icons"},
32 | { "path": "icons/icon-snow.jpg", "folder": "icons"},
33 | { "path": "icons/icon-storm.jpg", "folder": "icons"},
34 | { "path": "icons/icon-rain.jpg", "folder": "icons"},
35 | { "path": "icons/icon-cloud.jpg", "folder": "icons"},
36 | { "path": "icons/icon-sun_dark.jpg", "folder": "icons"},
37 | { "path": "icons/icon-snow_dark.jpg", "folder": "icons"},
38 | { "path": "icons/icon-storm_dark.jpg", "folder": "icons"},
39 | { "path": "icons/icon-rain_dark.jpg", "folder": "icons"},
40 | { "path": "icons/icon-cloud_dark.jpg", "folder": "icons"},
41 | { "path": "forms/Badger 2040 Test.odkbuild", "folder": "forms"}
42 |
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------