v1.7..152: DCF77 fixes (experimental)

This commit is contained in:
Klaus K Wilting 2019-02-04 20:02:30 +01:00
parent 39e2df7a05
commit 17cd82da68
9 changed files with 132 additions and 179 deletions

View File

@ -15,10 +15,6 @@
#include "rtctime.h" #include "rtctime.h"
#endif #endif
#ifdef HAS_DCF77
#include "dcf77.h"
#endif
void doHousekeeping(void); void doHousekeeping(void);
uint64_t uptime(void); uint64_t uptime(void);
void reset_counters(void); void reset_counters(void);

View File

@ -102,6 +102,7 @@ extern uint16_t volatile macs_total, macs_wifi, macs_ble,
batt_voltage; // display values batt_voltage; // display values
extern hw_timer_t *sendCycle, *displaytimer; extern hw_timer_t *sendCycle, *displaytimer;
extern SemaphoreHandle_t I2Caccess; extern SemaphoreHandle_t I2Caccess;
extern bool volatile BitsPending;
extern std::set<uint16_t, std::less<uint16_t>, Mallocator<uint16_t>> macs; extern std::set<uint16_t, std::less<uint16_t>, Mallocator<uint16_t>> macs;
extern std::array<uint64_t, 0xff>::iterator it; extern std::array<uint64_t, 0xff>::iterator it;

View File

@ -6,7 +6,7 @@
; ---> SELECT TARGET PLATFORM HERE! <--- ; ---> SELECT TARGET PLATFORM HERE! <---
[platformio] [platformio]
;env_default = generic env_default = generic
;env_default = ebox ;env_default = ebox
;env_default = eboxtube ;env_default = eboxtube
;env_default = heltec ;env_default = heltec
@ -24,16 +24,16 @@
;env_default = lolin32lora ;env_default = lolin32lora
;env_default = lolin32lite ;env_default = lolin32lite
;env_default = octopus32 ;env_default = octopus32
env_default = ebox, eboxtube, heltec, ttgobeam, lopy4, lopy, ttgov21old, ttgov21new, ttgofox ;env_default = ebox, eboxtube, heltec, ttgobeam, lopy4, lopy, ttgov21old, ttgov21new, ttgofox
; ;
description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around.
[common] [common]
; for release_version use max. 10 chars total, use any decimal format like "a.b.c" ; for release_version use max. 10 chars total, use any decimal format like "a.b.c"
release_version = 1.7.143 release_version = 1.7.152
; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running!
; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose
debug_level = 3 debug_level = 0
; UPLOAD MODE: select esptool to flash via USB/UART, select custom to upload to cloud for OTA ; UPLOAD MODE: select esptool to flash via USB/UART, select custom to upload to cloud for OTA
upload_protocol = esptool upload_protocol = esptool
;upload_protocol = custom ;upload_protocol = custom
@ -45,9 +45,9 @@ monitor_speed = 115200
lib_deps_lora = lib_deps_lora =
MCCI LoRaWAN LMIC library@^2.3.1 MCCI LoRaWAN LMIC library@^2.3.1
lib_deps_display = lib_deps_display =
U8g2@>=2.25.5 U8g2@>=2.25.7
lib_deps_rgbled = lib_deps_rgbled =
SmartLeds@>=1.1.3 SmartLeds@>=1.1.5
lib_deps_gps = lib_deps_gps =
TinyGPSPlus@>=1.0.2 TinyGPSPlus@>=1.0.2
lib_deps_rtc = lib_deps_rtc =

View File

@ -83,11 +83,6 @@ void doHousekeeping() {
bme_status.temperature, bme_status.iaq, bme_status.iaq_accuracy); bme_status.temperature, bme_status.iaq, bme_status.iaq_accuracy);
#endif #endif
// generate DCF77 timeframes
#ifdef HAS_DCF77
sendDCF77();
#endif
// check free heap memory // check free heap memory
if (ESP.getMinFreeHeap() <= MEM_LOW) { if (ESP.getMinFreeHeap() <= MEM_LOW) {
ESP_LOGI(TAG, ESP_LOGI(TAG,

View File

@ -1,13 +1,10 @@
//
// source:
// https://www.elektormagazine.com/labs/dcf77-emulator-with-esp8266-elektor-labs-version-150713
//
/* /*
Simulate a DCF77 radio receiver // Emulate a DCF77 radio receiver
Emit a complete three minute pulses train from the GPIO output //
the train is preceded by a single pulse and the lacking 59th pulse to allow // parts of this code werde adapted from source:
some clock model syncronization of the beginning frame. After the three pulses //
train one more single pulse is sent to safely close the frame https://www.elektormagazine.com/labs/dcf77-emulator-with-esp8266-elektor-labs-version-150713
//
*/ */
#if defined HAS_DCF77 #if defined HAS_DCF77
@ -18,11 +15,9 @@
static const char TAG[] = "main"; static const char TAG[] = "main";
TaskHandle_t DCF77Task; TaskHandle_t DCF77Task;
QueueHandle_t DCFSendQueue;
hw_timer_t *dcfCycle = NULL; hw_timer_t *dcfCycle = NULL;
#define DCF77_FRAME_SIZE 60 #define DCF77_FRAME_SIZE 60
#define DCF_FRAME_QUEUE_SIZE (HOMECYCLE / 60 + 1)
// array of dcf pulses for three minutes // array of dcf pulses for three minutes
uint8_t DCFtimeframe[DCF77_FRAME_SIZE]; uint8_t DCFtimeframe[DCF77_FRAME_SIZE];
@ -30,84 +25,31 @@ uint8_t DCFtimeframe[DCF77_FRAME_SIZE];
// initialize and configure DCF77 output // initialize and configure DCF77 output
int dcf77_init(void) { int dcf77_init(void) {
DCFSendQueue = xQueueCreate(DCF_FRAME_QUEUE_SIZE,
sizeof(DCFtimeframe) / sizeof(DCFtimeframe[0]));
if (!DCFSendQueue) {
ESP_LOGE(TAG, "Could not create DCF77 send queue. Aborting.");
return 0; // failure
}
ESP_LOGI(TAG, "DCF77 send queue created, size %d Bytes",
DCF_FRAME_QUEUE_SIZE * sizeof(DCFtimeframe) /
sizeof(DCFtimeframe[0]));
pinMode(HAS_DCF77, OUTPUT); pinMode(HAS_DCF77, OUTPUT);
digitalWrite(HAS_DCF77, LOW); digitalWrite(HAS_DCF77, HIGH);
xTaskCreatePinnedToCore(dcf77_loop, // task function
"dcf77loop", // name of task
2048, // stack size of task
(void *)1, // parameter of the task
3, // priority of the task
&DCF77Task, // task handle
0); // CPU core
assert(DCF77Task); // has dcf77 task started?
// setup 100ms clock signal for DCF77 generator using esp32 hardware timer 1
ESP_LOGD(TAG, "Starting DCF pulse...");
dcfCycle = timerBegin(1, 8000, true); // set 80 MHz prescaler to 1/10000 sec
timerAttachInterrupt(dcfCycle, &DCF77IRQ, true);
timerAlarmWrite(dcfCycle, 2000, true); // 100ms cycle
timerAlarmEnable(dcfCycle);
xTaskNotify(DCF77Task, 0, eNoAction);
return 1; // success return 1; // success
} // ifdcf77_init } // ifdcf77_init
// called every 100msec for DCF77 output
void DCF_Ticker() {
static uint8_t DCF_Frame[DCF77_FRAME_SIZE];
static uint8_t bit = 0;
static uint8_t pulse = 0;
static bool BitsPending = false;
while (BitsPending) {
switch (pulse++) {
case 0: // start of second -> start of timeframe for logic signal
if (DCF_Frame[bit] != dcf_off)
digitalWrite(HAS_DCF77, LOW);
return;
case 1: // 100ms after start of second -> end of timeframe for logic 0
if (DCF_Frame[bit] == dcf_zero)
digitalWrite(HAS_DCF77, HIGH);
return;
case 2: // 200ms after start of second -> end of timeframe for logic signal
digitalWrite(HAS_DCF77, HIGH);
return;
case 9: // last pulse before next second starts
pulse = 0;
if (bit++ != DCF77_FRAME_SIZE)
return;
else { // last pulse of DCF77 frame (59th second)
bit = 0;
BitsPending = false;
};
break;
}; // switch
}; // while
// get next frame to send from queue
if (xQueueReceive(DCFSendQueue, &DCF_Frame, (TickType_t)0) == pdTRUE)
BitsPending = true;
} // DCF_Ticker()
void dcf77_loop(void *pvParameters) {
configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check
// task remains in blocked state until it is notified by isr
for (;;) {
xTaskNotifyWait(
0x00, // don't clear any bits on entry
ULONG_MAX, // clear all bits on exit
NULL,
portMAX_DELAY); // wait forever (missing error handling here...)
DCF_Ticker();
}
vTaskDelete(DCF77Task); // shoud never be reached
} // dcf77_loop()
uint8_t dec2bcd(uint8_t dec, uint8_t startpos, uint8_t endpos, uint8_t dec2bcd(uint8_t dec, uint8_t startpos, uint8_t endpos,
uint8_t pArray[]) { uint8_t pArray[]) {
@ -123,7 +65,7 @@ uint8_t dec2bcd(uint8_t dec, uint8_t startpos, uint8_t endpos,
return parity; return parity;
} }
void enqueueTimeframe(time_t t) { void generateTimeframe(time_t t) {
uint8_t ParityCount; uint8_t ParityCount;
@ -156,21 +98,7 @@ void enqueueTimeframe(time_t t) {
// ENCODE TAIL (bit 59) // ENCODE TAIL (bit 59)
DCFtimeframe[59] = dcf_off; DCFtimeframe[59] = dcf_off;
// --> missing code here for switching second! // !! missing code here for leap second !!
/*
In unregelmäßigen Zeitabständen muss eine Schaltsekunde eingefügt werden. Dies
ist dadurch bedingt, dass sich die Erde nicht genau in 24 Stunden um sich
selbst dreht. Auf die koordinierte Weltzeitskala UTC bezogen, wird diese
Korrektur zum Ende der letzten Stunde des 31. Dezember oder 30. Juni
vorgenommen. In Mitteleuropa muss die Schaltsekunde daher am 1. Januar um 1.00
Uhr MEZ oder am 1.Juli um 2.00 MESZ eingeschoben werden. Zu den genannten
Zeiten werden daher 61 Sekunden gesendet.
*/
// post generated DCFtimeframe data to DCF SendQueue
if (xQueueSendToBack(DCFSendQueue, (void *)&DCFtimeframe[0], (TickType_t)0) !=
pdPASS)
ESP_LOGE(TAG, "Failed to send DCF data");
// for debug: print the DCF77 frame buffer // for debug: print the DCF77 frame buffer
char out[DCF77_FRAME_SIZE + 1]; char out[DCF77_FRAME_SIZE + 1];
@ -179,41 +107,80 @@ void enqueueTimeframe(time_t t) {
out[i] = DCFtimeframe[i] + '0'; // convert int digit to printable ascii out[i] = DCFtimeframe[i] + '0'; // convert int digit to printable ascii
} }
out[DCF77_FRAME_SIZE] = '\0'; // string termination char out[DCF77_FRAME_SIZE] = '\0'; // string termination char
ESP_LOGD(TAG, "DCF=%s", out); ESP_LOGD(TAG, "DCF Timeframe = %s", out);
} }
void sendDCF77() { // called every 100msec by hardware time
void DCF_Out() {
time_t t = now(); static uint8_t bit = 0;
static uint8_t pulse = 0;
/* if (!BitsPending) {
if (second(t) > 56) { // prepare next frame to send
delay(30000); generateTimeframe(now());
return; BitsPending = true;
// wait until next minute, then kick off hardware timer and first DCF pulse
do {
delay(2);
} while (second());
} }
*/
// enqueue DCF timeframes for each i minute // ticker out current frame
for (uint8_t i = 0; i < DCF_FRAME_QUEUE_SIZE; i++) while (BitsPending) {
enqueueTimeframe(t + i * 60); switch (pulse++) {
/* case 0: // start of second -> start of timeframe for logic signal
// how many to the minute end ? if (DCFtimeframe[bit] != dcf_off)
// don't forget that we begin transmission at second 58 digitalWrite(HAS_DCF77, LOW);
delay((58 - second(t)) * 1000); return;
// three minutes are needed to transmit all the packet case 1: // 100ms after start of second -> end of timeframe for logic 0
// then wait more 30 secs to locate safely at the half of minute if (DCFtimeframe[bit] == dcf_zero)
// NB 150+60=210sec, 60secs are lost from main routine digitalWrite(HAS_DCF77, HIGH);
delay(150000); return;
*/
} // Ende ReadAndDecodeTime() case 2: // 200ms after start of second -> end of timeframe for logic 1
digitalWrite(HAS_DCF77, HIGH);
return;
case 9: // last pulse before next second starts
pulse = 0;
if (bit++ != DCF77_FRAME_SIZE)
return;
else { // end of DCF77 frame (59th second)
bit = 0;
BitsPending = false;
};
break;
}; // switch
}; // while
} // DCF_Out()
// interrupt service routine triggered each 100ms by ESP32 hardware timer // interrupt service routine triggered each 100ms by ESP32 hardware timer
void IRAM_ATTR DCF77IRQ() { void IRAM_ATTR DCF77IRQ() {
xTaskNotifyFromISR(DCF77Task, xTaskGetTickCountFromISR(), eSetBits, NULL); xTaskNotifyFromISR(DCF77Task, 0, eNoAction, NULL);
portYIELD_FROM_ISR(); portYIELD_FROM_ISR();
} }
void dcf77_loop(void *pvParameters) {
configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check
// task remains in blocked state until it is notified by isr
for (;;) {
xTaskNotifyWait(
0x00, // don't clear any bits on entry
ULONG_MAX, // clear all bits on exit
NULL,
portMAX_DELAY); // wait forever (missing error handling here...)
DCF_Out();
}
BitsPending = false; // stop blink in display
vTaskDelete(DCF77Task); // shoud never be reached
} // dcf77_loop()
#endif // HAS_DCF77 #endif // HAS_DCF77

View File

@ -145,14 +145,14 @@ void refreshtheDisplay() {
uint8_t msgWaiting; uint8_t msgWaiting;
char buff[16]; // 16 chars line buffer char buff[16]; // 16 chars line buffer
#if defined HAS_RTC || defined HAS_GPS #if (defined HAS_DCF77) || (defined HAS_IF482)
const char timeNosyncSymbol = '?'; const char timeNosyncSymbol = '?';
#if defined HAS_IF482 || defined HAS_DCF77 #if (defined HAS_IF482)
const char timesyncSymbol = '+'; const char timesyncSymbol = '+';
#else #else
const char timesyncSymbol = '*'; const char timesyncSymbol = '*';
#endif #endif
#endif // HAS_RTC #endif
// update counter (lines 0-1) // update counter (lines 0-1)
snprintf( snprintf(
@ -217,21 +217,20 @@ void refreshtheDisplay() {
#ifdef HAS_LORA #ifdef HAS_LORA
u8x8.setCursor(0, 6); u8x8.setCursor(0, 6);
#if (!defined HAS_RTC) && (!defined HAS_GPS) #if (!defined HAS_DCF77) && (!defined HAS_IF482)
// update LoRa status display (line 6) // update LoRa status display (line 6)
u8x8.printf("%-16s", display_line6); u8x8.printf("%-16s", display_line6);
#else // HAS_RTC or HAS_GPS #else // we want a time display instead LoRa status
// update time/date display (line 6) // update time/date display (line 6)
time_t t = myTZ.toLocal(now()); time_t t = myTZ.toLocal(now());
char timeState = char timeState =
timeStatus() == timeSet ? timesyncSymbol : timeNosyncSymbol; timeStatus() == timeSet ? timesyncSymbol : timeNosyncSymbol;
#ifdef RTC_INT // make timestatus symbol blinking if pps line // make timestatus symbol blinking if pps line
if (second(t) % 2) if ((BitsPending) && (second(t) % 2))
timeState = ' '; timeState = ' ';
#endif // RTC_INT
u8x8.printf("%02d:%02d:%02d%c %2d.%3s", hour(t), minute(t), second(t), u8x8.printf("%02d:%02d:%02d%c %2d.%3s", hour(t), minute(t), second(t),
timeState, day(t), printmonth[month(t)]); timeState, day(t), printmonth[month(t)]);
#endif // HAS_RTC #endif
// update LMiC event display (line 7) // update LMiC event display (line 7)
u8x8.setCursor(0, 7); u8x8.setCursor(0, 7);

View File

@ -103,7 +103,20 @@ int if482_init(void) {
ESP_LOGE(TAG, "I2c bus busy - IF482 initialization error"); ESP_LOGE(TAG, "I2c bus busy - IF482 initialization error");
return 0; return 0;
} }
xTaskCreatePinnedToCore(if482_loop, // task function
"if482loop", // name of task
2048, // stack size of task
(void *)1, // parameter of the task
3, // priority of the task
&IF482Task, // task handle
0); // CPU core
assert(IF482Task); // has if482loop task started?
// setup external interupt for active low RTC INT pin
pinMode(RTC_INT, INPUT_PULLUP); pinMode(RTC_INT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(RTC_INT), IF482IRQ, FALLING);
return 1; return 1;
} // if482_init } // if482_init
@ -134,6 +147,8 @@ String if482Telegram(time_t tt) {
month(t), day(t), weekday(t), hour(t), minute(t), second(t)); month(t), day(t), weekday(t), hour(t), minute(t), second(t));
snprintf(out, sizeof out, "O%cL%s\r", mon, buf); snprintf(out, sizeof out, "O%cL%s\r", mon, buf);
ESP_LOGD(TAG, "IF482 = %s", out);
return out; return out;
} }
@ -153,6 +168,8 @@ void if482_loop(void *pvParameters) {
tt = now(); tt = now();
} while (t == tt); } while (t == tt);
BitsPending = true; // start blink in display
// take timestamp at moment of start of new second // take timestamp at moment of start of new second
const TickType_t shotTime = xTaskGetTickCount() - startTime - timeOffset; const TickType_t shotTime = xTaskGetTickCount() - startTime - timeOffset;
@ -169,6 +186,7 @@ void if482_loop(void *pvParameters) {
vTaskDelayUntil(&wakeTime, shotTime); // sets waketime to moment of shot vTaskDelayUntil(&wakeTime, shotTime); // sets waketime to moment of shot
IF482.print(if482Telegram(now() + 1)); IF482.print(if482Telegram(now() + 1));
} }
BitsPending = false; // stop blink in display
vTaskDelete(IF482Task); // shoud never be reached vTaskDelete(IF482Task); // shoud never be reached
} // if482_loop() } // if482_loop()

View File

@ -34,7 +34,7 @@
// faster or slower. This causes the transceiver to be earlier switched on, // faster or slower. This causes the transceiver to be earlier switched on,
// so consuming more power. You may sharpen (reduce) this value if you are // so consuming more power. You may sharpen (reduce) this value if you are
// limited on battery. // limited on battery.
#define CLOCK_ERROR_PROCENTAGE 30 #define CLOCK_ERROR_PROCENTAGE 3
// Set this to 1 to enable some basic debug output (using printf) about // Set this to 1 to enable some basic debug output (using printf) about
// RF settings used during transmission and reception. Set to 2 to // RF settings used during transmission and reception. Set to 2 to

View File

@ -34,7 +34,7 @@ spiloop 0 2 reads/writes data on spi interface
IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer
looptask 1 1 arduino core -> runs the LMIC LoRa stack looptask 1 1 arduino core -> runs the LMIC LoRa stack
irqhandler 1 1 executes tasks triggered by irq irqhandler 1 1 executes tasks triggered by hw irq, see table below
gpsloop 1 2 reads data from GPS via serial or i2c gpsloop 1 2 reads data from GPS via serial or i2c
bmeloop 1 1 reads data from BME sensor via i2c bmeloop 1 1 reads data from BME sensor via i2c
IDLE 1 0 ESP32 arduino scheduler -> runs wifi channel rotator IDLE 1 0 ESP32 arduino scheduler -> runs wifi channel rotator
@ -44,7 +44,7 @@ Low priority numbers denote low priority tasks.
Tasks using i2c bus all must have same priority, because using mutex semaphore Tasks using i2c bus all must have same priority, because using mutex semaphore
(irqhandler, bmeloop) (irqhandler, bmeloop)
ESP32 hardware timers ESP32 hardware irq timers
================================ ================================
0 triggers display refresh 0 triggers display refresh
1 triggers DCF77 clock signal 1 triggers DCF77 clock signal
@ -65,6 +65,7 @@ char display_line6[16], display_line7[16]; // display buffers
uint8_t volatile channel = 0; // channel rotation counter uint8_t volatile channel = 0; // channel rotation counter
uint16_t volatile macs_total = 0, macs_wifi = 0, macs_ble = 0, uint16_t volatile macs_total = 0, macs_wifi = 0, macs_ble = 0,
batt_voltage = 0; // globals for display batt_voltage = 0; // globals for display
bool volatile BitsPending = false; // DCF77 or IF482 ticker indicator
hw_timer_t *sendCycle = NULL, *homeCycle = NULL; hw_timer_t *sendCycle = NULL, *homeCycle = NULL;
#ifdef HAS_DISPLAY #ifdef HAS_DISPLAY
@ -97,7 +98,7 @@ void setup() {
char features[100] = ""; char features[100] = "";
I2Caccess = xSemaphoreCreateMutex(); // for access management of i2c bus I2Caccess = xSemaphoreCreateMutex(); // for access management of i2c bus
if ((I2Caccess) != NULL) if (I2Caccess)
xSemaphoreGive((I2Caccess)); // Flag the i2c bus available for use xSemaphoreGive((I2Caccess)); // Flag the i2c bus available for use
// disable brownout detection // disable brownout detection
@ -334,7 +335,6 @@ void setup() {
#if defined HAS_DCF77 #if defined HAS_DCF77
strcat_P(features, " DCF77"); strcat_P(features, " DCF77");
assert(dcf77_init());
#endif #endif
#if defined HAS_IF482 && defined RTC_INT #if defined HAS_IF482 && defined RTC_INT
@ -417,37 +417,14 @@ void setup() {
setSyncInterval(TIME_SYNC_INTERVAL_GPS * 60); setSyncInterval(TIME_SYNC_INTERVAL_GPS * 60);
#endif #endif
#if defined HAS_IF482 && defined RTC_INT #if defined HAS_IF482 && defined DCF_77
#error "You may define at most one of HAS_IF482 or DCF_77"
#elif defined HAS_IF482 && defined RTC_INT
ESP_LOGI(TAG, "Starting IF482 Generator..."); ESP_LOGI(TAG, "Starting IF482 Generator...");
xTaskCreatePinnedToCore(if482_loop, // task function assert(if482_init());
"if482loop", // name of task #elif defined HAS_DCF77
2048, // stack size of task
(void *)1, // parameter of the task
3, // priority of the task
&IF482Task, // task handle
0); // CPU core
// setup external interupt for active low RTC INT pin
assert(IF482Task != NULL); // has if482loop task started?
attachInterrupt(digitalPinToInterrupt(RTC_INT), IF482IRQ, FALLING);
#endif
#if defined HAS_DCF77
ESP_LOGI(TAG, "Starting DCF77 Generator..."); ESP_LOGI(TAG, "Starting DCF77 Generator...");
xTaskCreatePinnedToCore(dcf77_loop, // task function assert(dcf77_init());
"dcf77loop", // name of task
2048, // stack size of task
(void *)1, // parameter of the task
3, // priority of the task
&DCF77Task, // task handle
0); // CPU core
// setup 100ms clock signal for DCF77 generator using esp32 hardware timer 1
assert(DCF77Task != NULL); // has dcf77 task started?
dcfCycle = timerBegin(1, 8000, true);
timerAttachInterrupt(dcfCycle, &DCF77IRQ, true);
timerAlarmWrite(dcfCycle, 1000, true);
timerAlarmEnable(dcfCycle);
#endif #endif
} // setup() } // setup()