@@ -43,13 +43,24 @@ extern bool loadBMP(FatVolume *fs, char *filename, uint8_t *dest,
43
43
const float brightness, const float gamma);
44
44
45
45
// Some general global stuff -----------------------------------------------
46
- FatVolume *fs; // CIRCUITPY filesystem
47
- volatile bool core1_wait = true ; // For syncing RP2040's two cores
46
+ FatVolume *fs; // CIRCUITPY filesystem
47
+ volatile bool core1_wait = true ; // For syncing RP2040's two cores
48
+ int error_code = 0 ; // >= 1 on startup error
49
+ DeserializationError json_error; // For JSON-specific errors
50
+ struct {
51
+ char *msg; // Error message to Serial console
52
+ uint16_t ms; // LED blink rate, milliseconds
53
+ } err[] = {
54
+ " poi.cfg syntax error" , 250 , // json_error will be set
55
+ " poi.cfg not found" , 250 ,
56
+ " Can't access CIRCUITPY drive" , 100 ,
57
+ " Radio init error" , 500 ,
58
+ };
48
59
49
60
// DotStar- and image-related stuff ----------------------------------------
50
61
typedef struct { // Per-image structure:
51
- float reps_sec ; // Image repetitions per second (fractions OK)
52
- uint32_t total_usec; // Total display time, microseconds
62
+ uint32_t usec_pass ; // Time for each pass of image, microseconds
63
+ uint32_t total_usec; // Total display time of image , microseconds
53
64
int32_t height; // Image height in pixels
54
65
union {
55
66
char name[30 ]; // BMP filename (within path), not used after load
@@ -71,6 +82,8 @@ uint16_t num_images = 1; // Image count (1 for "off" image)
71
82
uint32_t total_height = 1 ; // Sum of image heights (1 for "off")
72
83
img *imglist = NULL ; // Image array (allocated before load)
73
84
uint8_t *linebuf = NULL ; // Data for ALL images
85
+ volatile uint16_t imgnum = 0 ;
86
+ volatile uint32_t last_change = 0 ;
74
87
// linebuf is dynamically allocated after config read to contain ALL POV
75
88
// data used in performance; images are NOT loaded dynamically as needed.
76
89
// This is to help ensure quick program change and keep all the poi in
@@ -87,147 +100,124 @@ uint8_t *linebuf = NULL; // Data for ALL images
87
100
#define NETWORKID 1
88
101
#define NODEID 255
89
102
#define INTERVAL 250000 // Broadcast interval, microseconds
103
+ #define LATENCY 1000 // Encode, xmit, decode microseconds
90
104
91
105
RH_RF69 *radio;
92
106
bool sender = false ; // Radio send vs receive
93
-
94
- volatile uint16_t imgnum = 0 ;
95
- volatile uint32_t last_change = 0 ;
96
- uint32_t last_xmit = 0 ;
107
+ uint32_t last_xmit = 0 ;
97
108
98
109
// The PRIMARY CORE runs all the non-deterministic grunt work --
99
110
// Filesystem/config init, talking to the radio and infrared,
100
111
// keeping the CIRCUITPY filesystem alive.
101
112
102
- void error_handler (const char *message, uint16_t speed) {
103
- Serial.print (" Error: " );
104
- Serial.println (message);
105
- if (speed) { // Fatal error, blink LED
106
- pinMode (LED_BUILTIN, OUTPUT);
107
- for (;;) {
108
- digitalWrite (LED_BUILTIN, (millis () / speed) & 1 );
109
- yield (); // Keep filesystem accessible for editing
110
- }
111
- } else { // Not fatal, just show message
112
- Serial.println (" Continuing with defaults" );
113
- }
114
- }
115
-
116
113
void setup () { // Core 0 start-up code
117
114
118
115
// Start the CIRCUITPY flash filesystem first. Very important!
119
116
fs = Adafruit_CPFS::begin ();
120
117
121
118
Serial.begin (115200 );
122
- // while (!Serial);
119
+ // while (!Serial);
120
+
121
+ if (fs) { // Filesystem OK?
122
+ StaticJsonDocument<1024 > doc;
123
+ FatFile file;
123
124
124
- if (fs == NULL ) {
125
- error_handler (" Can't access CIRCUITPY drive" , 0 );
126
- } else {
127
- StaticJsonDocument<1024 > doc;
128
- DeserializationError error;
129
- FatFile file;
130
-
131
125
// Open configuration file and attempt to decode JSON data within.
132
126
if ((file = fs->open (" poi.cfg" , FILE_READ))) {
133
- error = deserializeJson (doc, file);
127
+ json_error = deserializeJson (doc, file);
134
128
file.close ();
135
- } else {
136
- error_handler (" poi.cfg not found" , 0 );
137
- }
138
-
139
- if (error) {
140
- error_handler (" poi.cfg syntax error" , 0 );
141
- Serial.print (" JSON error: " );
142
- Serial.println (error.c_str ());
143
- } else {
144
- // Config is valid, override defaults in program variables...
145
- dotstar_length = doc[" dotstar_length" ] | dotstar_length;
146
- dotstar_clock = doc[" dotstar_clock" ] | dotstar_clock;
147
- dotstar_data = doc[" dotstar_data" ] | dotstar_data;
148
- JsonVariant v = doc[" dotstar_order" ];
149
- if (v.is <const char *>()) {
150
- const struct {
151
- const char *key; // String version of color order, e.g. "RGB"
152
- uint8_t val; // Numeric version of same, e.g. DOTSTAR_RGB
153
- uint8_t offset[3 ]; // Red, green, blue indices
154
- } dict[] = {
155
- " RGB" , DOTSTAR_RGB, { 0 , 1 , 2 },
156
- " RBG" , DOTSTAR_RBG, { 0 , 2 , 1 },
157
- " GRB" , DOTSTAR_GRB, { 1 , 0 , 2 },
158
- " GBR" , DOTSTAR_GBR, { 2 , 0 , 1 },
159
- " BRG" , DOTSTAR_BRG, { 1 , 2 , 0 },
160
- " BGR" , DOTSTAR_BGR, { 2 , 1 , 0 },
161
- };
162
- for (uint8_t i=0 ; i< sizeof dict / sizeof dict[0 ]; i++) {
163
- if (!strcasecmp (v, dict[i].key )) {
164
- dotstar_order = dict[i].val ;
165
- rOffset = dict[i].offset [0 ];
166
- gOffset = dict[i].offset [1 ];
167
- bOffset = dict[i].offset [2 ];
168
- break ;
129
+ if (!json_error) {
130
+ // Config is valid, override defaults in program variables...
131
+ dotstar_length = doc[" dotstar_length" ] | dotstar_length;
132
+ dotstar_clock = doc[" dotstar_clock" ] | dotstar_clock;
133
+ dotstar_data = doc[" dotstar_data" ] | dotstar_data;
134
+ dotstar_brightness = doc[" brightness" ] | dotstar_brightness;
135
+ dotstar_gamma = doc[" gamma" ] | dotstar_gamma;
136
+ JsonVariant v = doc[" dotstar_order" ];
137
+ if (v.is <const char *>()) { // Present and is a string?
138
+ const struct {
139
+ const char *key; // String version of color order, e.g. "RGB"
140
+ uint8_t val; // Numeric version of same, e.g. DOTSTAR_RGB
141
+ uint8_t offset[3 ]; // Red, green, blue indices
142
+ } dict[] = {
143
+ " RGB" , DOTSTAR_RGB, { 0 , 1 , 2 },
144
+ " RBG" , DOTSTAR_RBG, { 0 , 2 , 1 },
145
+ " GRB" , DOTSTAR_GRB, { 1 , 0 , 2 },
146
+ " GBR" , DOTSTAR_GBR, { 2 , 0 , 1 },
147
+ " BRG" , DOTSTAR_BRG, { 1 , 2 , 0 },
148
+ " BGR" , DOTSTAR_BGR, { 2 , 1 , 0 },
149
+ };
150
+ for (uint8_t i=0 ; i< sizeof dict / sizeof dict[0 ]; i++) {
151
+ if (!strcasecmp (v, dict[i].key )) {
152
+ dotstar_order = dict[i].val ;
153
+ rOffset = dict[i].offset [0 ];
154
+ gOffset = dict[i].offset [1 ];
155
+ bOffset = dict[i].offset [2 ];
156
+ break ;
157
+ }
169
158
}
170
159
}
171
- }
160
+
161
+ // Validate inputs; clip to ranges
162
+ rOffset = min (max (rOffset , 0 ), 2 );
163
+ gOffset = min (max (gOffset , 0 ), 2 );
164
+ bOffset = min (max (bOffset , 0 ), 2 );
165
+ dotstar_data = min (max (dotstar_data , 0 ), 29 );
166
+ dotstar_clock = min (max (dotstar_clock , 0 ), 29 );
167
+ dotstar_brightness = min (max (dotstar_brightness, 0.0 ), 255.0 );
168
+
169
+ sender = doc[" sender" ] | sender; // true if xmit, false if recv
170
+
171
+ v = doc[" path" ];
172
+ if (v.is <const char *>()) {
173
+ strncpy (path, v, sizeof path - 1 );
174
+ path[sizeof path - 1 ] = 0 ;
175
+ // Strip trailing / if present
176
+ int n = strlen (path) - 1 ;
177
+ while ((n >= 0 ) && (path[n] == ' /' )) path[n--] = 0 ;
178
+ }
172
179
173
- dotstar_brightness = doc[" brightness" ] | dotstar_brightness;
174
- dotstar_gamma = doc[" gamma" ] | dotstar_gamma;
175
-
176
- // Validate inputs; clip to ranges
177
- rOffset = min (max (rOffset , 0 ), 2 );
178
- gOffset = min (max (gOffset , 0 ), 2 );
179
- bOffset = min (max (bOffset , 0 ), 2 );
180
- dotstar_data = min (max (dotstar_data , 0 ), 29 );
181
- dotstar_clock = min (max (dotstar_clock , 0 ), 29 );
182
- dotstar_brightness = min (max (dotstar_brightness, 0.0 ), 255.0 );
183
-
184
- // Init DotStar ASAP, allows using LEDs as status display.
185
- // If Dotstar data and clock pins are on the same SPI instance,
186
- // and form a valid TX/SCK pair...
187
- if ((((dotstar_data / 8 ) & 1 ) == ((dotstar_clock / 8 ) & 1 )) &&
188
- ((dotstar_data & 3 ) == 3 ) && ((dotstar_clock & 3 ) == 2 )) {
189
- // Use hardware SPI for writing pixels. Most likely is spi0, NOT shared w/radio.
190
- spi_inst_t *inst = ((dotstar_data / 8 ) & 1 ) ? spi1 : spi0;
191
- SPIClassRP2040 *spi = new SPIClassRP2040 (spi0, -1 , -1 , dotstar_clock, dotstar_data);
192
- strip = new Adafruit_DotStar (dotstar_length, dotstar_order, (SPIClass *)spi);
193
- spi->beginTransaction (SPISettings (32000000 , MSBFIRST, SPI_MODE0));
194
- } else { // Use bitbang for writing pixels (slower, but any 2 pins)
195
- strip = new Adafruit_DotStar (dotstar_length, dotstar_data, dotstar_clock, dotstar_order);
196
- }
197
- strip->begin ();
198
- strip->show (); // Clear LEDs ASAP
199
-
200
- v = doc[" path" ];
201
- if (v.is <const char *>()) {
202
- strncpy (path, v, sizeof path - 1 );
203
- path[sizeof path - 1 ] = 0 ;
204
- // Strip trailing / if present
205
- int n = strlen (path) - 1 ;
206
- while ((n >= 0 ) && (path[n] == ' /' )) path[n--] = 0 ;
207
- }
208
- char filename[80 ];
209
- sender = doc[" sender" ] | sender;
210
- v = doc[" program" ];
211
- if (v.is <JsonArray>()) {
212
- num_images += v.size ();
213
- if ((imglist = (img *)malloc (num_images * sizeof (img)))) {
214
- for (int i=1 ; i<num_images; i++) {
215
- JsonVariant v2 = v[i - 1 ];
216
- if (v2.is <JsonArray>() && (v2.size () == 3 )) {
217
- strncpy (imglist[i].name , (char *)v2[0 ].as <const char *>(), sizeof imglist[i].name );
218
- imglist[i].name [sizeof imglist[i].name - 1 ] = 0 ;
219
- imglist[i].reps_sec = v2[1 ].as <float >();
220
- imglist[i].total_usec = (uint32_t )(1000000.0 * fabs (v2[2 ].as <float >()));
221
- sprintf (filename, " %s/%s" , path, imglist[i].name );
222
- if (bmpHeight (fs, filename, &imglist[i].height ))
223
- total_height += imglist[i].height ;
180
+ v = doc[" program" ];
181
+ if (v.is <JsonArray>()) {
182
+ num_images += v.size ();
183
+ if ((imglist = (img *)malloc (num_images * sizeof (img)))) {
184
+ for (int i=1 ; i<num_images; i++) {
185
+ JsonVariant v2 = v[i - 1 ];
186
+ if (v2.is <JsonArray>() && (v2.size () == 3 )) {
187
+ char filename[80 ];
188
+ strncpy (imglist[i].name , (char *)v2[0 ].as <const char *>(), sizeof imglist[i].name );
189
+ imglist[i].name [sizeof imglist[i].name - 1 ] = 0 ;
190
+ float reps_sec = v2[1 ].as <float >();
191
+ if (reps_sec > 0.0 ) {
192
+ imglist[i].usec_pass = (uint32_t )(1000000.0 / reps_sec);
193
+ } else {
194
+ imglist[i].usec_pass = 1 ;
195
+ }
196
+ imglist[i].total_usec = (uint32_t )(1000000.0 * fabs (v2[2 ].as <float >()));
197
+ sprintf (filename, " %s/%s" , path, imglist[i].name );
198
+ if (bmpHeight (fs, filename, &imglist[i].height ))
199
+ total_height += imglist[i].height ;
200
+ }
224
201
}
225
202
}
226
203
}
227
- }
228
- } // end JSON OK
229
- } // end filesystem OK
204
+ } else error_code = 1 ; // end JSON decode
205
+ } else error_code = 2 ; // end config file open
206
+ } else error_code = 3 ; // end filesys
207
+
208
+ // If Dotstar data and clock pins are on the same SPI instance,
209
+ // and form a valid TX/SCK pair...
210
+ if ((((dotstar_data / 8 ) & 1 ) == ((dotstar_clock / 8 ) & 1 )) &&
211
+ ((dotstar_data & 3 ) == 3 ) && ((dotstar_clock & 3 ) == 2 )) {
212
+ // Use hardware SPI for writing pixels. Most likely is spi0, NOT shared w/radio.
213
+ spi_inst_t *inst = ((dotstar_data / 8 ) & 1 ) ? spi1 : spi0;
214
+ SPIClassRP2040 *spi = new SPIClassRP2040 (spi0, -1 , -1 , dotstar_clock, dotstar_data);
215
+ strip = new Adafruit_DotStar (dotstar_length, dotstar_order, (SPIClass *)spi);
216
+ } else { // Use bitbang for writing pixels (slower, but any 2 pins)
217
+ strip = new Adafruit_DotStar (dotstar_length, dotstar_data, dotstar_clock, dotstar_order);
218
+ }
230
219
220
+ // If no image alloc above, make a single "off" image instance:
231
221
if (!imglist) imglist = (img *)malloc (sizeof (img));
232
222
233
223
if ((linebuf = (uint8_t *)calloc (dotstar_length * total_height, 3 ))) {
@@ -240,26 +230,34 @@ void setup() { // Core 0 start-up code
240
230
imglist[i].data = dest;
241
231
dest += imglist[i].height * dotstar_length * 3 ;
242
232
} else {
243
- // On image load error, set data to the "off" image:
233
+ // Image load error is not fatal; set data to the "off" image:
244
234
imglist[i].data = linebuf;
245
235
imglist[i].height = 1 ;
246
236
}
247
237
}
248
238
} else {
239
+ // If all-images alloc fails, alloc just the single "off" image:
249
240
linebuf = (uint8_t *)calloc (dotstar_length, 3 );
250
241
}
242
+
243
+ // Initialize the "off" image:
251
244
imglist[0 ].data = linebuf;
252
245
imglist[0 ].height = 1 ;
253
- imglist[0 ].reps_sec = 1.0 ;
246
+ imglist[0 ].usec_pass = 1 ;
254
247
imglist[0 ].total_usec = 1000000 ;
248
+
255
249
imgnum = 0 ;
256
250
last_change = micros ();
257
251
258
- core1_wait = false ; // Done reading config, core 1 can proceed
252
+ if (strip) {
253
+ strip->begin ();
254
+ core1_wait = false ; // All initialized, core 1 can proceed
255
+ }
259
256
260
257
radio = new RH_RF69 (RFM69_CS, RFM69_INT);
261
258
262
259
pinMode (LED_BUILTIN, OUTPUT);
260
+ digitalWrite (LED_BUILTIN, LOW);
263
261
pinMode (RFM69_RST, OUTPUT);
264
262
digitalWrite (RFM69_RST, LOW);
265
263
@@ -270,19 +268,30 @@ void setup() { // Core 0 start-up code
270
268
delay (10 );
271
269
272
270
// Initialize radio
273
- if (radio == NULL ) Serial.println (" OH NOES" );
274
- Serial.println (radio->init ());
275
- Serial.println (radio->setFrequency (915.0 ));
276
- radio->setTxPower (14 , true );
277
- radio->setEncryptionKey ((uint8_t *)ENCRYPTKEY);
271
+ if (radio) {
272
+ radio->init ();
273
+ radio->setFrequency (915.0 );
274
+ radio->setTxPower (14 , true );
275
+ radio->setEncryptionKey ((uint8_t *)ENCRYPTKEY);
276
+ } else error_code = 4 ;
278
277
}
279
278
280
- uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
279
+ uint8_t buf[RH_RF69_MAX_MESSAGE_LEN] __attribute__ ((aligned( 4 ))) ;
281
280
282
281
void loop () {
283
282
uint32_t now = micros ();
284
283
285
- if (sender) {
284
+ if (error_code) {
285
+ digitalWrite (LED_BUILTIN, (now / (err[error_code - 1 ].ms * 1000 )) & 1 );
286
+ if ((now - last_change) >= 1000000 ) {
287
+ Serial.printf (" ERROR: %s\r\n " , err[error_code - 1 ].msg );
288
+ if (json_error) {
289
+ Serial.print (" JSON error: " );
290
+ Serial.println (json_error.c_str ());
291
+ }
292
+ last_change = now;
293
+ }
294
+ } else if (sender) {
286
295
bool xmit = false ; // Will be set true if it's time to send
287
296
uint32_t elapsed = now - last_change;
288
297
@@ -296,9 +305,9 @@ void loop() {
296
305
}
297
306
if (xmit) {
298
307
radio->waitPacketSent ();
299
- memcpy (&buf[0 ], ( void *)&imgnum , 2 );
300
- memcpy (&buf[2 ], &elapsed , 2 );
301
- memcpy (&buf[6 ], &buf[0 ] , 6 ); // Rather than checksum, send data 2X
308
+ memcpy (&buf[0 ], &elapsed , 4 );
309
+ memcpy (&buf[4 ], ( void *)&imgnum , 2 );
310
+ memcpy (&buf[6 ], &buf[0 ] , 6 ); // Rather than checksum, send data 2X
302
311
last_xmit = now;
303
312
radio->send ((uint8_t *)buf, 12 );
304
313
}
@@ -309,10 +318,14 @@ void loop() {
309
318
Serial.print (" got reply: " );
310
319
Serial.println ((char *)buf);
311
320
if ((len == 12 ) && !memcmp (&buf[0 ], &buf[6 ], 6 )) {
312
- uint16_t n = *(uint16_t *)(&buf[0 ]);
321
+ uint32_t t = *(uint32_t *)(&buf[0 ]);
322
+ uint16_t n = *(uint16_t *)(&buf[4 ]);
313
323
if (n != imgnum) {
314
- imgnum = n;
315
- last_change = now - *(uint32_t *)(&buf[2 ]);
324
+ imgnum = n;
325
+ last_change = now - t - LATENCY;
326
+ } else {
327
+ // Compare last change time against
328
+ // our perception of elapsed time.
316
329
}
317
330
}
318
331
} else {
@@ -331,9 +344,10 @@ void setup1() {
331
344
while (core1_wait); // Wait for setup() to complete before going to loop1()
332
345
}
333
346
334
-
347
+ // TO DO: compensate for clock difference
335
348
void loop1 () {
336
- uint32_t row = (uint32_t )((float )(micros () - last_change) / 1000000.0 * imglist[imgnum].reps_sec * (float )imglist[imgnum].height ) % imglist[imgnum].height ;
349
+ // core1_wait won't release unless strip is non-NULL, this is OK...
350
+ uint32_t row = imglist[imgnum].height * ((micros () - last_change) % imglist[imgnum].usec_pass ) / imglist[imgnum].usec_pass ;
337
351
memcpy (strip->getPixels (), imglist[imgnum].data + row * dotstar_length * 3 , dotstar_length * 3 );
338
352
strip->show ();
339
353
}
0 commit comments