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

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