]> mj.ucw.cz Git - home-hw.git/blob - bsb/daemon/burrow-bsb.c
BSB: Daemon for Turris communicating via MQTT
[home-hw.git] / bsb / daemon / burrow-bsb.c
1 /*
2  *      Boiler System Bus Interface Daemon
3  *
4  *      (c) 2020 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <ucw/lib.h>
8 #include <ucw/log.h>
9 #include <ucw/opt.h>
10 #include <ucw/string.h>
11 #include <ucw/unaligned.h>
12
13 #include <stdarg.h>
14 #include <stdio.h>
15 #include <stdint.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <syslog.h>
19 #include <time.h>
20 #include <unistd.h>
21
22 #include <libusb.h>
23 #include <mosquitto.h>
24
25 typedef unsigned char byte;
26 typedef uint32_t u32;
27 typedef unsigned int uint;
28
29 #include "../firmware/interface.h"
30
31 /*** MQTT ***/
32
33 static struct mosquitto *mosq;
34 static bool mqtt_connected;
35
36 static void mqtt_publish(const char *topic, const char *fmt, ...)
37 {
38         va_list args;
39         va_start(args, fmt);
40
41         if (mqtt_connected) {
42                 char m[256];
43                 int l = vsnprintf(m, sizeof(m), fmt, args);
44                 int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true);
45                 if (err != MOSQ_ERR_SUCCESS)
46                         msg(L_ERROR, "Mosquitto: Publish failed, error=%d", err);
47         }
48
49         va_end(args);
50 }
51
52 static void mqtt_conn_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int status)
53 {
54         if (!status) {
55                 msg(L_DEBUG, "MQTT: Connection established");
56                 mqtt_publish("status/bsb", "ok");
57                 mqtt_connected = true;
58         } else if (mqtt_connected) {
59                 msg(L_DEBUG, "MQTT: Connection lost");
60                 mqtt_connected = false;
61         }
62 }
63
64 static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message)
65 {
66         msg(L_DEBUG, "MQTT(%d): %s", level, message);
67 }
68
69 static void mqtt_init(void)
70 {
71         mosquitto_lib_init();
72
73         mosq = mosquitto_new("bsbd", 1, NULL);
74         if (!mosq)
75                 die("Mosquitto: Initialization failed");
76
77         mosquitto_connect_callback_set(mosq, mqtt_conn_callback);
78         mosquitto_log_callback_set(mosq, mqtt_log_callback);
79
80         if (mosquitto_will_set(mosq, "status/bsb", 4, "dead", 0, true) != MOSQ_ERR_SUCCESS)
81                 die("Mosquitto: Unable to set will");
82
83         if (mosquitto_connect_async(mosq, "127.0.0.1", 1883, 60) != MOSQ_ERR_SUCCESS)
84                 die("Mosquitto: Unable to connect");
85
86         if (mosquitto_loop_start(mosq))
87                 die("Mosquitto: Cannot start service thread");
88 }
89
90 /*** USB ***/
91
92 static struct libusb_context *usb_ctxt;
93 static struct libusb_device_handle *devh;
94
95 static void usb_error(const char *msg, ...)
96 {
97         va_list args;
98         va_start(args, msg);
99         ucw_vmsg(L_ERROR, msg, args);
100         fputc('\n', stderr);
101         va_end(args);
102
103         if (devh) {
104                 libusb_close(devh);
105                 devh = NULL;
106         }
107 }
108
109 static void open_device(void)
110 {
111         int err;
112         libusb_device **devlist;
113         ssize_t devn = libusb_get_device_list(usb_ctxt, &devlist);
114         if (devn < 0)
115                 die("Cannot enumerate USB devices: error %d", (int) devn);
116
117         for (ssize_t i=0; i<devn; i++) {
118                 struct libusb_device_descriptor desc;
119                 libusb_device *dev = devlist[i];
120                 if (!libusb_get_device_descriptor(dev, &desc)) {
121                         if (desc.idVendor == BSB_USB_VENDOR && desc.idProduct == BSB_USB_PRODUCT) {
122                                 msg(L_INFO, "Found device at usb%d.%d", libusb_get_bus_number(dev), libusb_get_device_address(dev));
123
124                                 if (err = libusb_open(dev, &devh)) {
125                                         usb_error("Cannot open device: error %d", err);
126                                         goto out;
127                                 }
128                                 libusb_reset_device(devh);
129                                 if (err = libusb_claim_interface(devh, 0)) {
130                                         usb_error("Cannot claim interface: error %d", err);
131                                         goto out;
132                                 }
133
134                                 goto out;
135                         }
136                 }
137         }
138
139 out:
140         libusb_free_device_list(devlist, 1);
141 }
142
143 static void init_usb(void)
144 {
145         int err;
146         if (err = libusb_init(&usb_ctxt))
147                 die("Cannot initialize libusb: error %d", err);
148         // libusb_set_debug(usb_ctxt, 3);
149         open_device();
150
151 }
152
153 /*** Protocol ***/
154
155 static const char * const stat_names[] = {
156 #define P(x) #x,
157         BSB_STATS
158 #undef P
159 };
160
161 static void process_stats(time_t t, byte *resp, uint len)
162 {
163         for (uint i=0; i < ARRAY_SIZE(stat_names) && 4*i + 3 < (uint) len; i++) {
164                 char item[64];
165                 snprintf(item, sizeof(item), "burrow/bsb/stats/%s", stat_names[i]);
166                 mqtt_publish(item, "%u", (uint) get_u32_le(resp+4*i), t);
167         }
168 }
169
170 static void process_info(byte *p, uint len)
171 {
172         if (len < 4)
173                 return;
174
175         u32 addr = get_u32_be(p);
176         p += 4;
177         len -= 4;
178
179         switch (addr) {
180                 case 0x05000219:
181                         if (len >= 4) {
182                                 uint temp = get_u16_be(p);
183                                 uint press = get_u16_be(p + 2);
184                                 mqtt_publish("burrow/heating/outside-temp", "%.2f", temp / 64.);
185                                 mqtt_publish("burrow/heating/water-pressure", "%.1f", press / 10.);
186                         }
187                         break;
188                 case 0x05000229:
189                         // AGU.2 status
190                         if (len >= 2) {
191                                 uint temp = get_u16_be(p);
192                                 mqtt_publish("burrow/heating/circuit1/mix-temp", "%.2f", temp / 64.);
193                         }
194                         break;
195                 case 0x05040227:
196                         // AGU.2 control
197                         if (len >= 2) {
198                                 uint m = get_u16_be(p);
199                                 mqtt_publish("burrow/heating/circuit1/mix-valve", "%u", m);
200                         }
201                         break;
202                 case 0x3d2d0215:
203                         // Room 1 status
204                         if (len >= 2) {
205                                 uint temp = get_u16_be(p);
206                                 mqtt_publish("burrow/heating/circuit1/room-temp", "%.2f", temp / 64.);
207                         }
208                         break;
209                 case 0x3e2e0215:
210                         // Room 2 status
211                         if (len >= 2) {
212                                 uint temp = get_u16_be(p);
213                                 mqtt_publish("burrow/heating/circuit2/room-temp", "%.2f", temp / 64.);
214                         }
215                         break;
216         }
217 }
218
219 static void process_answer(byte *p, uint len)
220 {
221         if (len < 4)
222                 return;
223
224         u32 addr = get_u32_be(p);
225         p += 4;
226         len -= 4;
227
228         switch (addr) {
229         }
230 }
231
232 static void process_frame(time_t t, byte *pkt, uint len)
233 {
234         if (!len) {
235                 msg(L_ERROR, "Received empty frame");
236                 return;
237         }
238
239         byte status = *pkt++;
240         len--;
241
242         if (!len) {
243                 msg(L_ERROR, "Frame transmit status: %u", status);
244                 return;
245         }
246
247         if (len < 6) {
248                 msg(L_ERROR, "Received truncated frame");
249                 return;
250         }
251
252         char hex[3*len + 1];
253         mem_to_hex(hex, pkt, len, ' ');
254         mqtt_publish("bsb/frame", "%s", hex, t);
255
256         msg(L_DEBUG, "<< %s", hex);
257
258         byte *params = pkt + BF_PARAMS;
259         uint param_len = len - BF_PARAMS - 2;   // 2 for CRC
260
261         switch (pkt[BF_OP]) {
262                 case BSB_OP_INFO:
263                         process_info(params, param_len);
264                         break;
265                 case BSB_OP_ANSWER:
266                         process_answer(params, param_len);
267                         break;
268         }
269 }
270
271 static int use_daemon;
272 static int use_debug;
273
274 static struct opt_section options = {
275         OPT_ITEMS {
276                 OPT_HELP("A daemon for controlling the air conditioning controller via MQTT"),
277                 OPT_HELP(""),
278                 OPT_HELP("Options:"),
279                 OPT_BOOL('d', "debug", use_debug, 0, "\tLog debugging messages"),
280                 OPT_BOOL(0, "daemon", use_daemon, 0, "\tDaemonize"),
281                 OPT_HELP_OPTION,
282                 OPT_CONF_OPTIONS,
283                 OPT_END
284         }
285 };
286
287 int main(int argc UNUSED, char **argv)
288 {
289         log_init(argv[0]);
290         opt_parse(&options, argv+1);
291
292         if (use_daemon) {
293                 struct log_stream *ls = log_new_syslog("daemon", LOG_PID);
294                 log_set_default_stream(ls);
295         }
296         if (!use_debug)
297                 log_default_stream()->levels &= ~(1U << L_DEBUG);
298
299         mqtt_init();
300         init_usb();
301
302         time_t now = time(NULL);
303         time_t last_stats = 0;
304         // time_t last_query = now;
305         int err, received;
306         byte resp[64];
307
308         for (;;) {
309                 if (!devh) {
310                         msg(L_INFO, "Waiting for device to appear...");
311                         while (!devh) {
312                                 sleep(5);
313                                 open_device();
314                         }
315                 }
316
317                 now = time(NULL);
318                 if (last_stats + 60 < now) {
319                         if ((received = libusb_control_transfer(devh, 0xc0, 0x00, 0, 0, resp, sizeof(resp), 1000)) < 0) {
320                                 usb_error("Receive failed: error %d", received);
321                                 continue;
322                         }
323
324                         process_stats(now, resp, received);
325                         last_stats = now;
326                 }
327
328 #if 0
329                 if (last_query + 10 < now) {
330                         byte pkt[] = { 0xdc, 0xc2, 0x00, 0x0b, 0x06, 0x3d, 0x2e, 0x11, 0x25, 0x00, 0x00 };
331                         if (err = libusb_bulk_transfer(devh, 0x01, pkt, sizeof(pkt), &received, 2000)) {
332                                 usb_error("Send failed: error %d", err);
333                                 continue;
334                         } else {
335                                 // msg(L_DEBUG"Send OK: %d bytes", received);
336                         }
337                         last_query = now;
338                 }
339 #endif
340
341                 if (err = libusb_interrupt_transfer(devh, 0x82, resp, 64, &received, 1000)) {
342                         if (err != LIBUSB_ERROR_TIMEOUT)
343                                 usb_error("Receive failed: error %d", err);
344                         continue;
345                 }
346
347                 process_frame(now, resp, received);
348         }
349 }