···
3
3
.DS_Store
4
4
/.balena-sync.yml
5
5
/dist/jan-poka
6
6
+
.pio
7
7
+
.vscode
···
12
12
platform = espressif32
13
13
board = firebeetle32
14
14
framework = arduino
15
15
+
lib_deps =
16
16
+
waspinator/AccelStepper@^1.61
17
17
+
knolleary/PubSubClient@^2.8
18
18
+
bblanchon/ArduinoJson@^6.18.5
19
19
+
makuna/NeoPixelBus@^2.6.9
20
20
+
amcewen/HttpClient@^2.2.0
21
21
+
upload_port = /dev/cu.usbserial-0001
22
22
+
monitor_port = /dev/cu.usbserial-0001
23
23
+
monitor_speed = 115200
···
1
1
+
#include <Arduino.h>
1
2
#include <AccelStepper.h>
2
3
#include <NeoPixelBus.h>
3
4
#include <PubSubClient.h>
···
26
27
27
28
NeoPixelBus<NeoGrbwFeature, NeoSk6812Method> leds(LED_COUNT, 27);
28
29
29
29
-
HTTPClient httpClient;
30
30
WiFiClient wifiClient;
31
31
PubSubClient mqttClient(wifiClient);
32
32
time_t epoch;
···
36
36
int sunriseMins = -1;
37
37
int sunsetMins = -1;
38
38
39
39
+
// TODO: Pick good colours; include Warm White?
40
40
+
RgbwColor dayCol(128, 64, 0, 128);
41
41
+
RgbwColor nightCol(0, 0, 128, 0);
42
42
+
RgbwColor offCol(0, 0, 0, 0);
43
43
+
44
44
+
void setupWifi();
45
45
+
void setupMQTT();
46
46
+
void setupTime();
47
47
+
void setupMotors();
48
48
+
void setupLEDs();
49
49
+
50
50
+
void updateTime();
51
51
+
void updateClock();
52
52
+
void updateMotors();
53
53
+
void updateLEDs();
54
54
+
void updateMQTT();
55
55
+
56
56
+
void handleGeoTarget(char*, byte*, unsigned int);
57
57
+
bool steppersMoving();
58
58
+
void setSunriseAndSunset(String, String);
59
59
+
int timeToDayMins(int, int);
60
60
+
61
61
+
int updatedInt();
62
62
+
bool noNeedToUpdate();
63
63
+
void setTimezone(double, double);
64
64
+
void normalizeStepper(AccelStepper*);
65
65
+
void moveCircular(AccelStepper*, long);
66
66
+
39
67
void setupWifi() {
40
68
WiFi.begin(WIFI_SSID, WIFI_PASS);
41
69
while(WiFi.status()!=WL_CONNECTED) {
···
43
71
}
44
72
}
45
73
74
74
+
void updateMQTT() {
75
75
+
if (!mqttClient.connected()) {
76
76
+
if (!mqttClient.connect(APP_NAME, MQTT_USER, MQTT_PASS)) {
77
77
+
Serial.println("Failed to connect to MQTT broker");
78
78
+
// TODO: Backoff
79
79
+
return;
80
80
+
}
81
81
+
mqttClient.setCallback(handleGeoTarget);
82
82
+
mqttClient.subscribe(MQTT_TOPIC);
83
83
+
}
84
84
+
mqttClient.loop();
85
85
+
}
86
86
+
46
87
void setupMQTT() {
47
88
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
48
48
-
mqttConnectionLoop();
89
89
+
updateMQTT();
49
90
}
50
91
51
92
void setupTime() {
···
55
96
56
97
// Home:
57
98
setTimezone(51.53647542276452, -0.08639983104800102);
58
58
-
// Chris:
59
59
-
//setTimezone(49.27103836424926, -123.15245006680813);
60
60
-
// Venezuela
61
61
-
//setTimezone(10.436620855606654, -66.84207546938133);
62
99
}
63
100
64
101
void setupMotors() {
···
75
112
leds.Begin();
76
113
}
77
114
78
78
-
void setup() {
79
79
-
Serial.begin(115200);
80
80
-
81
81
-
setupWifi();
82
82
-
setupMotors();
83
83
-
setupLEDs();
84
84
-
setupTime();
85
85
-
setupMQTT();
86
86
-
Serial.println("Booted");
87
87
-
}
88
88
-
89
89
-
void loop() {
90
90
-
time(&epoch);
91
91
-
now = localtime(&epoch);
92
92
-
93
93
-
stepMin.run();
94
94
-
stepHour.run();
95
95
-
96
96
-
updateClock();
97
97
-
}
98
98
-
99
99
-
void mqttConnectionLoop() {
100
100
-
if (!mqttClient.connected()) {
101
101
-
if (!mqttClient.connect(APP_NAME, MQTT_USER, MQTT_PASS)) {
102
102
-
Serial.println("Failed to connect to MQTT broker");
103
103
-
// TODO: Backoff
104
104
-
return;
105
105
-
}
106
106
-
Serial.println("MQTT connected");
107
107
-
mqttClient.setCallback(handleGeoTarget);
108
108
-
mqttClient.subscribe(MQTT_TOPIC);
109
109
-
}
110
110
-
mqttClient.loop();
111
111
-
}
112
112
-
113
115
void handleGeoTarget(char* topic, byte* payload, unsigned int length) {
116
116
+
Serial.println("MQTT message received");
114
117
/* Copied from EspMQTTClient: https://github.com/plapointe6/EspMQTTClient/blob/master/src/EspMQTTClient.cpp#L649 */
115
118
// Convert the payload into a String
116
119
// First, We ensure that we dont bypass the maximum size of the PubSubClient library buffer that originated the payload
···
127
130
String payloadStr((char*)payload);
128
131
/* end */
129
132
130
130
-
StaticJsonDocument<512> jsonDoc;
131
131
-
DeserializationError err = deserializeJson(jsonDoc, payload);
133
133
+
StaticJsonDocument<512> doc;
134
134
+
DeserializationError err = deserializeJson(doc, payload);
132
135
if (err != DeserializationError::Ok) {
133
136
Serial.print("Unsuccessful at parsing MQTT JSON: ");
134
134
-
Serial.println(err.f_str());
137
137
+
Serial.println(err.c_str());
135
138
return;
136
139
}
137
137
-
setTimezone(jsonDoc["lat"], jsonDoc["lng"]);
140
140
+
setTimezone(doc["lat"], doc["lng"]);
138
141
}
139
142
140
143
bool steppersMoving() {
141
144
return stepMin.distanceToGo() != 0 || stepHour.distanceToGo() != 0;
142
145
}
143
146
144
144
-
void setTimezone(double latitude, double longitude) {
145
145
-
char url[255];
146
146
-
sprintf(url, TIMEZONE_QUERY, latitude, longitude);
147
147
-
148
148
-
httpClient.useHTTP10(true);
149
149
-
httpClient.begin(wifiClient, url);
150
150
-
httpClient.GET();
151
151
-
152
152
-
StaticJsonDocument<384> doc;
153
153
-
DeserializationError error = deserializeJson(doc, httpClient.getStream());
154
154
-
155
155
-
if (error) {
156
156
-
Serial.print("Grabbing timezone details failed: ");
157
157
-
Serial.println(error.c_str());
158
158
-
return;
159
159
-
}
160
160
-
161
161
-
// TODO: Deal with DST
162
162
-
configTime(doc["rawOffset"].as<float>() * 3600, 0, NTP_SERVER);
163
163
-
setSunriseAndSunset(doc["sunrise"], doc["sunset"]);
164
164
-
165
165
-
httpClient.end();
166
166
-
updateClock();
167
167
-
}
168
168
-
169
147
void setSunriseAndSunset(String sunriseStr, String sunsetStr) {
170
148
sunriseMins = timeToDayMins(sunriseStr.substring(11, 13).toInt(), sunriseStr.substring(14, 16).toInt());
171
149
sunsetMins = timeToDayMins(sunsetStr.substring(11, 13).toInt(), sunsetStr.substring(14, 16).toInt());
···
179
157
if (now == 0 || steppersMoving() || noNeedToUpdate())
180
158
return;
181
159
182
182
-
Serial.print("Setting time to: "); Serial.print(now->tm_hour); Serial.print(":"); Serial.println(now->tm_min);
160
160
+
Serial.print("Setting time to: "); Serial.print(now->tm_hour); Serial.print(":"); Serial.print(now->tm_min); Serial.print(":"); Serial.println(now->tm_sec);
183
161
184
162
updateMotors();
185
163
updateLEDs();
···
200
178
moveCircular(&stepHour, (now->tm_hour + now->tm_min/60.0) * HOURS_STEPS);
201
179
}
202
180
203
203
-
// TODO: Pick good colours; include Warm White?
204
204
-
RgbwColor dayCol(128, 64, 0, 128);
205
205
-
RgbwColor nightCol(0, 0, 128, 0);
206
206
-
RgbwColor offCol(0, 0, 0, 0);
207
207
-
208
181
void updateLEDs() {
209
182
int nowMins = timeToDayMins(now->tm_hour, now->tm_min);
210
183
// The time in minutes now, scaled to fit into the number of LEDs, offset by half as 0th pixel is at 6 o'clock
···
225
198
leds.Show();
226
199
}
227
200
201
201
+
void normalizeStepper(AccelStepper* stepper) {
202
202
+
long pos = stepper->currentPosition();
203
203
+
if (pos < 0 || pos >= MOTOR_STEPS) {
204
204
+
int mult = pos / MOTOR_STEPS;
205
205
+
stepper->setCurrentPosition((pos - mult * MOTOR_STEPS) % MOTOR_STEPS);
206
206
+
}
207
207
+
}
208
208
+
228
209
void moveCircular(AccelStepper* stepper, long steps) {
229
210
normalizeStepper(stepper);
230
211
···
240
221
stepper->moveTo(-newPos); // -ve for clockwise correction
241
222
}
242
223
243
243
-
void normalizeStepper(AccelStepper* stepper) {
244
244
-
long pos = stepper->currentPosition();
245
245
-
if (pos < 0 || pos >= MOTOR_STEPS) {
246
246
-
int mult = pos / MOTOR_STEPS;
247
247
-
stepper->setCurrentPosition((pos - mult * MOTOR_STEPS) % MOTOR_STEPS);
224
224
+
void setTimezone(double latitude, double longitude) {
225
225
+
Serial.print("Setting timezone for: ");
226
226
+
Serial.print(latitude);
227
227
+
Serial.print(", ");
228
228
+
Serial.println(longitude);
229
229
+
230
230
+
char url[255];
231
231
+
sprintf(url, TIMEZONE_QUERY, latitude, longitude);
232
232
+
233
233
+
HTTPClient httpClient;
234
234
+
httpClient.useHTTP10(true);
235
235
+
httpClient.begin(wifiClient, url);
236
236
+
httpClient.GET();
237
237
+
238
238
+
StaticJsonDocument<512> doc;
239
239
+
DeserializationError err = deserializeJson(doc, httpClient.getStream());
240
240
+
if (err != DeserializationError::Ok) {
241
241
+
Serial.print("Grabbing timezone details failed: ");
242
242
+
Serial.println(err.c_str());
243
243
+
return;
248
244
}
245
245
+
246
246
+
// TODO: Deal with DST
247
247
+
configTime(doc["rawOffset"].as<float>() * 3600, 0, NTP_SERVER);
248
248
+
setSunriseAndSunset(doc["sunrise"], doc["sunset"]);
249
249
+
250
250
+
httpClient.end();
251
251
+
updateClock();
252
252
+
}
253
253
+
254
254
+
void updateTime() {
255
255
+
time(&epoch);
256
256
+
now = localtime(&epoch);
257
257
+
}
258
258
+
259
259
+
void setup() {
260
260
+
Serial.begin(115200);
261
261
+
262
262
+
setupWifi();
263
263
+
setupMotors();
264
264
+
setupLEDs();
265
265
+
setupTime();
266
266
+
setupMQTT();
267
267
+
Serial.println("Booted");
268
268
+
}
269
269
+
270
270
+
void loop() {
271
271
+
updateTime();
272
272
+
273
273
+
stepMin.run();
274
274
+
stepHour.run();
275
275
+
276
276
+
updateClock();
277
277
+
updateMQTT();
249
278
}