Server - Slight fixes to coke handler to cope with the machine being dodge
[tpg/opendispense2.git] / src / server / handler_coke.c
1 /*
2  * OpenDispense 2 
3  * UCC (University [of WA] Computer Club) Electronic Accounting System
4  *
5  * handler_coke.c - Coke controller code
6  *
7  * This file is licenced under the 3-clause BSD Licence. See the file
8  * COPYING for full details.
9  *
10  * NOTES:
11  * - Remember, the coke machine echoes your text back to you!
12  */
13 #include "common.h"
14 #include <stdio.h>
15 #include <string.h>
16 #include <unistd.h>
17 #include <sys/stat.h>
18 #include <fcntl.h>
19 #include <regex.h>
20 #include <stdarg.h>
21 #include <pthread.h>
22
23 #define READ_TIMEOUT    2       // 2 seconds for ReadChar
24 #define TRACE_COKE      0
25
26 #if TRACE_COKE
27 # define TRACE(v...) do{printf("%s: ",__func__);printf(v);}while(0)
28 #else
29 # define TRACE(...)
30 #endif
31
32 // === IMPORTS ===
33
34 // === PROTOTYPES ===
35  int    Coke_InitHandler();
36  int    Coke_int_GetSlotStatus(char *Buffer, int Slot);
37 void    Coke_int_UpdateSlotStatuses(void);
38  int    Coke_CanDispense(int User, int Item);
39  int    Coke_DoDispense(int User, int Item);
40  int    Writef(const char *Format, ...);
41  int    WaitForColon();
42  int    ReadLine(int len, char *output);
43
44 // === GLOBALS ===
45 tHandler        gCoke_Handler = {
46         "coke",
47         Coke_InitHandler,
48         Coke_CanDispense,
49         Coke_DoDispense
50 };
51 char    *gsCoke_SerialPort = "/dev/ttyS0";
52  int    giCoke_SerialFD;
53 regex_t gCoke_StatusRegex;
54  int    gaCoke_CachedStatus[7];
55 pthread_mutex_t gCoke_Mutex = PTHREAD_MUTEX_INITIALIZER;
56 time_t  gtCoke_LastDispenseTime;
57
58 // == CODE ===
59 int Coke_InitHandler()
60 {
61         CompileRegex(&gCoke_StatusRegex, "^slot\\s+([0-9]+)\\s+([^:]+):([a-zA-Z]+)\\s*", REG_EXTENDED);
62         
63         printf("Connecting to coke machine on '%s'\n", gsCoke_SerialPort);
64         
65         giCoke_SerialFD = InitSerial(gsCoke_SerialPort, 9600);
66         if( giCoke_SerialFD == -1 ) {
67                 fprintf(stderr, "ERROR: Unable to open coke serial port ('%s')\n", gsCoke_SerialPort);
68         }
69         else {
70                 int i;
71                 for( i = 0; i < 7; i ++ )
72                         gaCoke_CachedStatus[i] = -1;
73                 // Reset the slot names.
74                 // - Dunno why this is needed, but the machine plays silly
75                 //   sometimes.
76                 Writef("\r\n\r\n");
77                 WaitForColon();
78                 Writef("n0 Slot0\r\n");
79                 if( !WaitForColon() )
80                 {
81                         Writef("n1 Slot1\r\n");
82                         WaitForColon();
83                         Writef("n2 Slot2\r\n");
84                         WaitForColon();
85                         Writef("n3 Slot3\r\n");
86                         WaitForColon();
87                         Writef("n4 Slot4\r\n");
88                         WaitForColon();
89                         Writef("n5 Slot5\r\n");
90                         WaitForColon();
91                         Writef("n6 Coke\r\n");
92                         WaitForColon();
93                         
94                         Coke_int_UpdateSlotStatuses();
95                 }
96                 else
97                         fprintf(stderr, "Coke machine timed out.\n");
98         }
99         
100         AddPeriodicFunction(Coke_int_UpdateSlotStatuses);
101         
102         return 0;
103 }
104
105 int Coke_int_GetSlotStatus(char *Buffer, int Slot)
106 {
107         regmatch_t      matches[4];
108          int    ret;
109         char    *status;        
110         
111         // Parse status response
112         ret = RunRegex(&gCoke_StatusRegex, Buffer, sizeof(matches)/sizeof(matches[0]), matches, "Bad Response");
113         if( ret ) {
114                 return -1;
115         }
116
117         // Get slot status
118         Buffer[ matches[3].rm_eo ] = '\0';
119         status = &Buffer[ matches[3].rm_so ];
120         
121         TRACE("Machine responded slot %i status '%s'\n", Slot, status);
122
123         if( strcmp(status, "full") == 0 ) {
124                 gaCoke_CachedStatus[Slot] = 0;  // 0: Avaliiable
125                 return 0;
126         }
127         else {
128                 gaCoke_CachedStatus[Slot] = 1;  // 1: Empty
129                 return 1;
130         }
131 }
132
133 /**
134  * \brief Update the status of all coke slots
135  * \note Uses goto to reduce the chance of the lock being kept
136  */
137 void Coke_int_UpdateSlotStatuses(void)
138 {
139          int    i, len;
140         char    tmp[40];
141         
142         if( giCoke_SerialFD == -1 )     return ;
143         
144         pthread_mutex_lock(&gCoke_Mutex);
145         
146         while( ReadLine(sizeof tmp, tmp) >= 0 ) ;
147         TRACE("send d7\n");
148         Writef("d7\r\n");       // Update slot statuses
149         if( WaitForColon() )    goto ret;
150         TRACE("send s\n");
151         Writef("s\r\n");
152         do {
153                 i = ReadLine(sizeof tmp, tmp);  // Read back what we just said
154                 if( i == -1 ) {
155                         TRACE("Eat read failed");
156                         goto ret;
157                 }
158         } while(tmp[0] != ':' && tmp[1] != 's');
159         
160         for( i = 0; i <= 6; i ++ )
161         {
162                 // Read until non-blank line
163                 while( (len = ReadLine(sizeof tmp, tmp)) == 0 ) ;
164                 if( len == -1 ) {
165                         TRACE("Read failed on slot %i\n", i);
166                         goto ret;       // I give up :(
167                 }
168                 TRACE("tmp = '%s'\n", tmp);
169                 Coke_int_GetSlotStatus(tmp, i);
170         }
171         // Eat blank line
172         len = ReadLine(sizeof tmp, tmp);
173         if( len == -1 ) {
174                 TRACE("Read failed on blank line\n");
175         }
176
177         TRACE("Updated\n");
178
179 ret:
180         pthread_mutex_unlock(&gCoke_Mutex);
181 }
182
183 int Coke_CanDispense(int UNUSED(User), int Item)
184 {
185         // Sanity please
186         if( Item < 0 || Item > 6 )      return -1;      // -EYOURBAD
187         
188         // Can't dispense if the machine is not connected
189         if( giCoke_SerialFD == -1 )
190                 return -2;
191
192         // HACK!
193         // 2011-10-21: The sensors in slot 1 and 3 a bad, just ignore the sensor result
194         if( Item == 1 || Item == 3 )    return 0;
195
196         return gaCoke_CachedStatus[Item];
197 }
198
199 /**
200  * \brief Actually do a dispense from the coke machine
201  */
202 int Coke_DoDispense(int UNUSED(User), int Item)
203 {
204         char    tmp[32];
205          int    ret, len;
206
207         // Sanity please
208         if( Item < 0 || Item > 6 )      return -1;
209
210         // Can't dispense if the machine is not connected
211         if( giCoke_SerialFD == -1 )
212                 return -2;
213
214         // Make sure there are not two dispenses within n seconds
215         if( time(NULL) - gtCoke_LastDispenseTime < 10 )
216         {
217                 printf("Wait %li seconds?\n", 10 - (time(NULL) - gtCoke_LastDispenseTime));
218                 sleep( 10 - (time(NULL) - gtCoke_LastDispenseTime) );
219                 printf("wait done\n");
220         }
221         
222         // LOCK
223         pthread_mutex_lock(&gCoke_Mutex);
224         
225         TRACE("flushing input\n");
226         
227
228         // Wait for prompt
229         ret = 0;
230         while( WaitForColon() && ret < 3 )
231         {
232                 // Flush the input buffer
233                 char    tmpbuf[512];
234                 read(giCoke_SerialFD, tmpbuf, sizeof(tmpbuf));
235                 TRACE("sending 'd7'\n");
236                 Writef("d7\r\n");
237                 ret ++;
238         }
239         if( ret == 3 )
240         {
241                 TRACE("timed out\n");
242                 pthread_mutex_unlock(&gCoke_Mutex);
243                 return -1;
244         }
245
246         TRACE("sending 'd%i'\n", Item);
247         // Dispense
248         Writef("d%i\r\n", Item);
249         
250         // Read empty lines and echo-backs
251         do {
252                 ret = ReadLine(sizeof(tmp)-1, tmp);
253                 if( ret == -1 ) {
254                         pthread_mutex_unlock(&gCoke_Mutex);
255                         return -1;
256                 }
257                 TRACE("read %i '%s'\n", ret, tmp);
258         } while( ret == 0 || tmp[0] == ':' || tmp[0] == 'd' );
259
260         WaitForColon(); // Eat up rest of response
261         
262         TRACE("done\n");
263
264         // TODO: Regex instead?
265         if( strcmp(tmp, "ok") == 0 ) {
266                 // We think dispense worked
267                 // - The machine returns 'ok' if an empty slot is dispensed, even if
268                 //   it doesn't actually try to dispense (no sound)
269                 ret = 0;
270         }
271         else {
272                 printf("Coke_DoDispense: Machine returned unknown value '%s'\n", tmp);
273                 ret = -1;
274         }
275         
276         TRACE("Updating slot status\n");
277         
278         // Update status
279         WaitForColon();
280         Writef("s%i\r\n", Item);
281         len = ReadLine(sizeof tmp, tmp);
282         if(len == -1)   gaCoke_CachedStatus[Item] = -1;
283         Coke_int_GetSlotStatus(tmp, Item);
284         {
285                 char buf[512];
286                 read(giCoke_SerialFD, buf, 512);        // Flush
287         }
288
289         gtCoke_LastDispenseTime = time(NULL);
290         
291         // Release and return
292         pthread_mutex_unlock(&gCoke_Mutex);
293         
294         //return ret;
295         // HACK!!!
296         return 0;
297 }
298
299 char ReadChar()
300 {
301         fd_set  readfs;
302         char    ch = 0;
303          int    ret;
304         struct timeval  timeout;
305         
306         timeout.tv_sec = READ_TIMEOUT;
307         timeout.tv_usec = 0;
308         
309         FD_ZERO(&readfs);
310         FD_SET(giCoke_SerialFD, &readfs);
311         
312         ret = select(giCoke_SerialFD+1, &readfs, NULL, NULL, &timeout);
313         if( ret == 0 ) {
314                 fprintf(stderr, "ReadChar: Timeout of %is expired\n", READ_TIMEOUT);
315                 return 0;       // Timeout
316         }
317         if( ret != 1 ) {
318                 printf("ReadChar: select return %i\n", ret);
319                 return 0;
320         }
321         
322         ret = read(giCoke_SerialFD, &ch, 1);
323         if( ret != 1 ) {
324                 printf("ReadChar: ret != 1 (%i)\n", ret);
325                 return 0;
326         }
327         
328         return ch;
329 }
330
331 int Writef(const char *Format, ...)
332 {
333         va_list args;
334          int    len;
335         
336         va_start(args, Format);
337         len = vsnprintf(NULL, 0, Format, args);
338         va_end(args);
339         
340         {
341                 char    buf[len+1];
342                 va_start(args, Format);
343                 vsnprintf(buf, len+1, Format, args);
344                 va_end(args);
345                 
346                 #if DEBUG
347                 printf("Writef: %s", buf);
348                 #endif
349                 
350                 return write(giCoke_SerialFD, buf, len);
351         }
352         
353 }
354
355 int WaitForColon()
356 {
357         fd_set  readfs;
358         char    ch = 0;
359         
360         FD_SET(giCoke_SerialFD, &readfs);
361         
362         while( (ch = ReadChar()) != ':' && ch != 0);
363         
364         if( ch == 0 )   return -1;      // Timeout
365         
366         return 0;
367 }
368
369 int ReadLine(int len, char *output)
370 {
371         char    ch;
372          int    i = 0;
373         
374         for(;;)
375         {
376                 ch = ReadChar();
377                         
378                 if( i < len )
379                         output[i++] = ch;
380                 
381                 if( ch == '\0' ) {
382                         break;
383                 }
384                 if( ch == '\n' || ch == '\r' ) {
385                         if( i < len )
386                                 output[--i] = '\0';
387                         break;
388                 }
389         }
390
391         if( !ch )       return -1;
392         return i;
393 }
394
395

UCC git Repository :: git.ucc.asn.au