Skip to content

Commit 0a1ee78

Browse files
Handle errors with more grace
1 parent 7c650e0 commit 0a1ee78

File tree

1 file changed

+152
-138
lines changed

1 file changed

+152
-138
lines changed

rfm-poi/rfm-poi.ino

Lines changed: 152 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -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 ----------------------------------------
5061
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
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)
7182
uint32_t total_height = 1; // Sum of image heights (1 for "off")
7283
img *imglist = NULL; // Image array (allocated before load)
7384
uint8_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

91105
RH_RF69 *radio;
92106
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;
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-
116113
void 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

282281
void 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
335348
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;
337351
memcpy(strip->getPixels(), imglist[imgnum].data + row * dotstar_length * 3, dotstar_length * 3);
338352
strip->show();
339353
}

0 commit comments

Comments
 (0)