@@ -43,13 +43,24 @@ extern bool loadBMP(FatVolume *fs, char *filename, uint8_t *dest,
4343 const float brightness, const float gamma);
4444
4545// 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+ };
4859
4960// DotStar- and image-related stuff ----------------------------------------
5061typedef 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
5364 int32_t height; // Image height in pixels
5465 union {
5566 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)
7182uint32_t total_height = 1 ; // Sum of image heights (1 for "off")
7283img *imglist = NULL ; // Image array (allocated before load)
7384uint8_t *linebuf = NULL ; // Data for ALL images
85+ volatile uint16_t imgnum = 0 ;
86+ volatile uint32_t last_change = 0 ;
7487// linebuf is dynamically allocated after config read to contain ALL POV
7588// data used in performance; images are NOT loaded dynamically as needed.
7689// 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
87100#define NETWORKID 1
88101#define NODEID 255
89102#define INTERVAL 250000 // Broadcast interval, microseconds
103+ #define LATENCY 1000 // Encode, xmit, decode microseconds
90104
91105RH_RF69 *radio;
92106bool 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 ;
97108
98109// The PRIMARY CORE runs all the non-deterministic grunt work --
99110// Filesystem/config init, talking to the radio and infrared,
100111// keeping the CIRCUITPY filesystem alive.
101112
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-
116113void setup () { // Core 0 start-up code
117114
118115 // Start the CIRCUITPY flash filesystem first. Very important!
119116 fs = Adafruit_CPFS::begin ();
120117
121118 Serial.begin (115200 );
122- // while (!Serial);
119+ // while (!Serial);
120+
121+ if (fs) { // Filesystem OK?
122+ StaticJsonDocument<1024 > doc;
123+ FatFile file;
123124
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-
131125 // Open configuration file and attempt to decode JSON data within.
132126 if ((file = fs->open (" poi.cfg" , FILE_READ))) {
133- error = deserializeJson (doc, file);
127+ json_error = deserializeJson (doc, file);
134128 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+ }
169158 }
170159 }
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+ }
172179
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+ }
224201 }
225202 }
226203 }
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+ }
230219
220+ // If no image alloc above, make a single "off" image instance:
231221 if (!imglist) imglist = (img *)malloc (sizeof (img));
232222
233223 if ((linebuf = (uint8_t *)calloc (dotstar_length * total_height, 3 ))) {
@@ -240,26 +230,34 @@ void setup() { // Core 0 start-up code
240230 imglist[i].data = dest;
241231 dest += imglist[i].height * dotstar_length * 3 ;
242232 } 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:
244234 imglist[i].data = linebuf;
245235 imglist[i].height = 1 ;
246236 }
247237 }
248238 } else {
239+ // If all-images alloc fails, alloc just the single "off" image:
249240 linebuf = (uint8_t *)calloc (dotstar_length, 3 );
250241 }
242+
243+ // Initialize the "off" image:
251244 imglist[0 ].data = linebuf;
252245 imglist[0 ].height = 1 ;
253- imglist[0 ].reps_sec = 1.0 ;
246+ imglist[0 ].usec_pass = 1 ;
254247 imglist[0 ].total_usec = 1000000 ;
248+
255249 imgnum = 0 ;
256250 last_change = micros ();
257251
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+ }
259256
260257 radio = new RH_RF69 (RFM69_CS, RFM69_INT);
261258
262259 pinMode (LED_BUILTIN, OUTPUT);
260+ digitalWrite (LED_BUILTIN, LOW);
263261 pinMode (RFM69_RST, OUTPUT);
264262 digitalWrite (RFM69_RST, LOW);
265263
@@ -270,19 +268,30 @@ void setup() { // Core 0 start-up code
270268 delay (10 );
271269
272270 // 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 ;
278277}
279278
280- uint8_t buf[RH_RF69_MAX_MESSAGE_LEN];
279+ uint8_t buf[RH_RF69_MAX_MESSAGE_LEN] __attribute__ ((aligned( 4 ))) ;
281280
282281void loop () {
283282 uint32_t now = micros ();
284283
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) {
286295 bool xmit = false ; // Will be set true if it's time to send
287296 uint32_t elapsed = now - last_change;
288297
@@ -296,9 +305,9 @@ void loop() {
296305 }
297306 if (xmit) {
298307 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
302311 last_xmit = now;
303312 radio->send ((uint8_t *)buf, 12 );
304313 }
@@ -309,10 +318,14 @@ void loop() {
309318 Serial.print (" got reply: " );
310319 Serial.println ((char *)buf);
311320 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 ]);
313323 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.
316329 }
317330 }
318331 } else {
@@ -331,9 +344,10 @@ void setup1() {
331344 while (core1_wait); // Wait for setup() to complete before going to loop1()
332345}
333346
334-
347+ // TO DO: compensate for clock difference
335348void 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 ;
337351 memcpy (strip->getPixels (), imglist[imgnum].data + row * dotstar_length * 3 , dotstar_length * 3 );
338352 strip->show ();
339353}
0 commit comments