├── .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 | ![/img/clk1.png](/img/apps_provision_01.jpg) 57 | ![/img/clk1.png](/img/apps_provision_02.jpg) 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 | ![/img/clk1.png](/img/barchart.jpg) 67 | ![/img/clk1.png](/img/heatmap_matrix.jpg) 68 | ![/img/clk1.png](/img/heatmap_summary.jpg) 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 | ![/img/clk1.png](/img/clk1.png) 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 | ![/img/clk2.png](/img/clk2.png) 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 | ![/img/dash.jpeg](/img/dash.jpeg) 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 | ![/img/clk2.png](/img/space.jpeg) 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 | ![/img/logger_1.jpeg](/img/logger_1.jpeg) 152 | ![/img/logger_2.jpeg](/img/logger_2.jpeg) 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 | ![/img/weather.png](/img/weather.png) 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 | ![/img/authenticator.png](/img/authenticator.jpg) 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 | ![/img/3d_print_case.png](/img/3d_print_case.png) 181 | ![/img/3d_print_case_2.jpeg](/img/3d_print_case_2.jpeg) 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"" 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 | --------------------------------------------------------------------------------