a34d88b9603f720114f877c1282ccc4cc56695bd
[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
57 // == CODE ===
58 int Coke_InitHandler()
59 {
60         CompileRegex(&gCoke_StatusRegex, "^slot\\s+([0-9]+)\\s+([^:]+):([a-zA-Z]+)\\s*", REG_EXTENDED);
61         
62         printf("connecting to coke machine...\n");
63         
64         giCoke_SerialFD = InitSerial(gsCoke_SerialPort, 9600);
65         if( giCoke_SerialFD == -1 ) {
66                 fprintf(stderr, "ERROR: Unable to open coke serial port ('%s')\n", gsCoke_SerialPort);
67         }
68         else {
69                 int i;
70                 for( i = 0; i < 7; i ++ )
71                         gaCoke_CachedStatus[i] = -1;
72                 // Reset the slot names.
73                 // - Dunno why this is needed, but the machine plays silly
74                 //   sometimes.
75                 Writef("n0 Slot0\n");
76                 if( !WaitForColon() )
77                 {
78                         Writef("n1 Slot1\n");
79                         WaitForColon();
80                         Writef("n2 Slot2\n");
81                         WaitForColon();
82                         Writef("n3 Slot3\n");
83                         WaitForColon();
84                         Writef("n4 Slot4\n");
85                         WaitForColon();
86                         Writef("n5 Slot5\n");
87                         WaitForColon();
88                         Writef("n6 Coke\n");
89                         
90                         Coke_int_UpdateSlotStatuses();
91                 }
92         }
93         
94         AddPeriodicFunction(Coke_int_UpdateSlotStatuses);
95         
96         return 0;
97 }
98
99 int Coke_int_GetSlotStatus(char *Buffer, int Slot)
100 {
101         regmatch_t      matches[4];
102          int    ret;
103         char    *status;        
104         
105         // Parse status response
106         ret = RunRegex(&gCoke_StatusRegex, Buffer, sizeof(matches)/sizeof(matches[0]), matches, "Bad Response");
107         if( ret ) {
108                 return -1;
109         }
110
111         // Get slot status
112         Buffer[ matches[3].rm_eo ] = '\0';
113         status = &Buffer[ matches[3].rm_so ];
114         
115         TRACE("Machine responded slot %i status '%s'\n", Slot, status);
116
117         if( strcmp(status, "full") == 0 ) {
118                 gaCoke_CachedStatus[Slot] = 0;  // 0: Avaliiable
119                 return 0;
120         }
121         else {
122                 gaCoke_CachedStatus[Slot] = 1;  // 1: Empty
123                 return 1;
124         }
125 }
126
127 /**
128  * \brief Update the status of all coke slots
129  * \note Uses goto to reduce the chance of the lock being kept
130  */
131 void Coke_int_UpdateSlotStatuses(void)
132 {
133          int    i, len;
134         char    tmp[40];
135         
136         if( giCoke_SerialFD == -1 )     return ;
137         
138         pthread_mutex_lock(&gCoke_Mutex);
139         
140         while( ReadLine(sizeof tmp, tmp) >= 0 ) ;
141         TRACE("send d7\n");
142         Writef("d7\r\n");       // Update slot statuses
143         if( WaitForColon() )    goto ret;
144         TRACE("send s\n");
145         Writef("s\r\n");
146         do {
147                 i = ReadLine(sizeof tmp, tmp);  // Read back what we just said
148                 if( i == -1 ) {
149                         TRACE("Eat read failed");
150                         goto ret;
151                 }
152         } while(tmp[0] != ':' && tmp[1] != 's');
153         
154         for( i = 0; i <= 6; i ++ )
155         {
156                 len = ReadLine(sizeof tmp, tmp);
157                 if( len == -1 ) {
158                         TRACE("Read failed on slot %i\n", i);
159                         goto ret;       // I give up :(
160                 }
161                 TRACE("tmp = '%s'\n", tmp);
162                 Coke_int_GetSlotStatus(tmp, i);
163         }
164         // Eat blank line
165         len = ReadLine(sizeof tmp, tmp);
166         if( len == -1 ) {
167                 TRACE("Read failed on blank line\n");
168         }
169
170         TRACE("Updated\n");
171
172 ret:
173         pthread_mutex_unlock(&gCoke_Mutex);
174 }
175
176 int Coke_CanDispense(int UNUSED(User), int Item)
177 {
178         // Sanity please
179         if( Item < 0 || Item > 6 )      return -1;      // -EYOURBAD
180         
181         // Can't dispense if the machine is not connected
182         if( giCoke_SerialFD == -1 )
183                 return -2;
184         
185         return gaCoke_CachedStatus[Item];
186 }
187
188 /**
189  * \brief Actually do a dispense from the coke machine
190  */
191 int Coke_DoDispense(int UNUSED(User), int Item)
192 {
193         char    tmp[32];
194          int    ret, len;
195
196         // Sanity please
197         if( Item < 0 || Item > 6 )      return -1;
198
199         // Can't dispense if the machine is not connected
200         if( giCoke_SerialFD == -1 )
201                 return -2;
202         
203         // LOCK
204         pthread_mutex_lock(&gCoke_Mutex);
205         
206         TRACE("flushing input\n");
207         
208
209         {
210                 char buf[512];
211                 while( ReadLine(512, buf) != -1 );
212         }
213                 
214         // Wait for prompt
215         ret = 0;
216         while( WaitForColon() && ret < 3 )
217         {
218                 // Flush the input buffer
219                 char    tmpbuf[512];
220                 read(giCoke_SerialFD, tmpbuf, sizeof(tmpbuf));
221                 TRACE("sending 'd7'\n");
222                 Writef("d7\r\n");
223                 ret ++;
224         }
225         if( ret == 3 )
226         {
227                 TRACE("timed out\n");
228                 pthread_mutex_unlock(&gCoke_Mutex);
229                 return -1;
230         }
231
232         TRACE("sending 'd%i'\n", Item);
233         // Dispense
234         Writef("d%i\r\n", Item);
235         
236         // Read empty lines and echo-backs
237         do {
238                 ret = ReadLine(sizeof(tmp)-1, tmp);
239                 if( ret == -1 ) {
240                         pthread_mutex_unlock(&gCoke_Mutex);
241                         return -1;
242                 }
243                 TRACE("read %i '%s'\n", ret, tmp);
244         } while( ret == 0 || tmp[0] == ':' || tmp[0] == 'd' );
245
246         WaitForColon(); // Eat up rest of response
247         
248         TRACE("done\n");
249
250         // TODO: Regex instead?
251         if( strcmp(tmp, "ok") == 0 ) {
252                 // We think dispense worked
253                 // - The machine returns 'ok' if an empty slot is dispensed, even if
254                 //   it doesn't actually try to dispense (no sound)
255                 ret = 0;
256         }
257         else {
258                 printf("Coke_DoDispense: Machine returned unknown value '%s'\n", tmp);
259                 ret = -1;
260         }
261         
262         TRACE("Updating slot status\n");
263         
264         // Update status
265         Writef("s%i\r\n", Item);
266         len = ReadLine(sizeof tmp, tmp);
267         if(len == -1)   gaCoke_CachedStatus[Item] = -1;
268         Coke_int_GetSlotStatus(tmp, Item);
269         {
270                 char buf[512];
271                 read(giCoke_SerialFD, buf, 512);        // Flush
272         }
273         
274         // Release and return
275         pthread_mutex_unlock(&gCoke_Mutex);
276         
277         //return ret;
278         // HACK!!!
279         return 0;
280 }
281
282 char ReadChar()
283 {
284         fd_set  readfs;
285         char    ch = 0;
286          int    ret;
287         struct timeval  timeout;
288         
289         timeout.tv_sec = READ_TIMEOUT;
290         timeout.tv_usec = 0;
291         
292         FD_ZERO(&readfs);
293         FD_SET(giCoke_SerialFD, &readfs);
294         
295         ret = select(giCoke_SerialFD+1, &readfs, NULL, NULL, &timeout);
296         if( ret == 0 )  return 0;       // Timeout
297         if( ret != 1 ) {
298                 printf("ReadChar: select return %i\n", ret);
299                 return 0;
300         }
301         
302         ret = read(giCoke_SerialFD, &ch, 1);
303         if( ret != 1 ) {
304                 printf("ReadChar: ret != 1 (%i)\n", ret);
305                 return 0;
306         }
307         
308         return ch;
309 }
310
311 int Writef(const char *Format, ...)
312 {
313         va_list args;
314          int    len;
315         
316         va_start(args, Format);
317         len = vsnprintf(NULL, 0, Format, args);
318         va_end(args);
319         
320         {
321                 char    buf[len+1];
322                 va_start(args, Format);
323                 vsnprintf(buf, len+1, Format, args);
324                 va_end(args);
325                 
326                 #if DEBUG
327                 printf("Writef: %s", buf);
328                 #endif
329                 
330                 return write(giCoke_SerialFD, buf, len);
331         }
332         
333 }
334
335 int WaitForColon()
336 {
337         fd_set  readfs;
338         char    ch = 0;
339         
340         FD_SET(giCoke_SerialFD, &readfs);
341         
342         while( (ch = ReadChar()) != ':' && ch != 0);
343         
344         if( ch == 0 )   return -1;      // Timeout
345         
346         return 0;
347 }
348
349 int ReadLine(int len, char *output)
350 {
351         char    ch;
352          int    i = 0;
353         
354         for(;;)
355         {
356                 ch = ReadChar();
357                         
358                 if( i < len )
359                         output[i++] = ch;
360                 
361                 if( ch == '\0' ) {
362                         break;
363                 }
364                 if( ch == '\n' || ch == '\r' ) {
365                         if( i < len )
366                                 output[--i] = '\0';
367                         break;
368                 }
369         }
370
371         if( !ch )       return -1;
372         return i;
373 }
374
375

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