From 906a1b933de48c046e47188e06a5468a7f7a6843 Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Tue, 13 Apr 2021 08:53:25 +1000 Subject: [PATCH 01/10] chore: update includes - I removed the weird Think C headers which were causing some issues with the types in VSCode --- include/MacIncludes.h | 10 +- include/ThinkCIncludes/BDC.h | 28 --- include/ThinkCIncludes/LoMem.h | 203 ----------------- include/ThinkCIncludes/SANE.h | 371 ------------------------------- include/ThinkCIncludes/SetUpA4.h | 42 ---- include/ThinkCIncludes/THINK.h | 116 ---------- include/ThinkCIncludes/asm.h | 141 ------------ include/ThinkCIncludes/pascal.h | 26 --- 8 files changed, 5 insertions(+), 932 deletions(-) delete mode 100644 include/ThinkCIncludes/BDC.h delete mode 100644 include/ThinkCIncludes/LoMem.h delete mode 100644 include/ThinkCIncludes/SANE.h delete mode 100644 include/ThinkCIncludes/SetUpA4.h delete mode 100644 include/ThinkCIncludes/THINK.h delete mode 100644 include/ThinkCIncludes/asm.h delete mode 100644 include/ThinkCIncludes/pascal.h diff --git a/include/MacIncludes.h b/include/MacIncludes.h index 41dc909..824d8bf 100644 --- a/include/MacIncludes.h +++ b/include/MacIncludes.h @@ -114,7 +114,7 @@ #endif // BDC -#if 1 +#if 0 #include #endif @@ -489,22 +489,22 @@ #endif // pascal.h -#if 1 +#if 0 #include #endif // asm.h -#if 1 +#if 0 #include #endif // LoMem -#if 1 +#if 0 #include #endif // THINK -#if 1 +#if 0 #include #endif diff --git a/include/ThinkCIncludes/BDC.h b/include/ThinkCIncludes/BDC.h deleted file mode 100644 index 6f83177..0000000 --- a/include/ThinkCIncludes/BDC.h +++ /dev/null @@ -1,28 +0,0 @@ - -/* - * BDC.h - binary/decimal conversion - * - * Copyright (c) 1991 Symantec Corporation. All rights reserved. - * - * These interfaces are based on material copyrighted - * by Apple Computer, Inc., 1985-1991. - * - * This material has been extracted from , so that it - * can be used without dragging in the International Utilities and - * Script Manager interfaces. - * - */ - -#ifndef __BDC__ -#define __BDC__ - -#ifndef __TYPES__ -#include -#endif - -pascal void StringToNum(ConstStr255Param theString,long *theNum); -void stringtonum(char *theString,long *theNum); -pascal void NumToString(long theNum,Str255 theString); -void numtostring(long theNum,char *theString); - -#endif // __BDC__ diff --git a/include/ThinkCIncludes/LoMem.h b/include/ThinkCIncludes/LoMem.h deleted file mode 100644 index 013a7b6..0000000 --- a/include/ThinkCIncludes/LoMem.h +++ /dev/null @@ -1,203 +0,0 @@ - -/* - * LoMem.h - definitions of lo-mem globals - * - * Copyright (c) 1991 Symantec Corporation. All rights reserved. - * - * These interfaces are based on material copyrighted - * by Apple Computer, Inc., 1985-1991. - * - */ - -#ifndef __LOMEM__ -#define __LOMEM__ - -#ifndef __TYPES__ -#include -#endif - -#ifndef __CONTROLS__ -#include -#endif - -#ifndef __DEVICES__ -#include -#endif - -#ifndef __DISKINIT__ -#include -#endif - -#ifndef __FONTS__ -#include -#endif - -#ifndef __MEMORY__ -#include -#endif - -#ifndef __MENUS__ -#include -#endif - -#ifndef __QUICKDRAW__ -#include -#endif - -#ifndef __SCRAP__ -#include -#endif - -#ifndef __WINDOWS__ -#include -#endif - - -short ApFontID : 0x984; -Ptr ApplLimit : 0x130; -THz ApplZone : 0x2AA; -Handle AppPacks[] : 0xAB8; -Handle AppParmHandle : 0xAEC; -short AtMenuBottom : 0xA0C; -AuxCtlHandle AuxCtlHead : 0xCD4; -AuxWinHandle AuxWinHead : 0xCD0; - -short BootDrive : 0x210; -Ptr BufPtr : 0x10C; - -long CaretTime : 0x2F4; -char CPUFlag : 0x12F; -Rect CrsrPin : 0x834; -WindowPtr CurActivate : 0xA64; -Str31 CurApName : 0x910; -short CurApRefNum : 0x900; -WindowPtr CurDeactive : 0xA68; -long CurDirStore : 0x398; -short CurJTOffset : 0x934; -short CurMap : 0xA5A; -short CurPageOption : 0x936; -Ptr CurrentA5 : 0x904; -Ptr CurStackBase : 0x908; - -StringHandle DAStrings[4] : 0xAA0; -long DefltStack : 0x322; -VCB *DefVCBPtr : 0x352; -ProcPtr DeskHook : 0xA6C; -Pattern DeskPattern : 0xA3C; -GDHandle DeviceList : 0x8A8; -long DoubleTime : 0x2F0; -ProcPtr DragHook : 0x9F6; -Pattern DragPattern : 0xA34; -QHdr DrvQHdr : 0x308; -short DSErrCode : 0xAF0; - -ProcPtr EjectNotify : 0x338; -QHdr EventQueue : 0x14A; -ProcPtr ExtStsDT[] : 0x2BE; - -Ptr FCBSPtr : 0x34E; -Str15 FinderName : 0x2E0; -HFSDefaults *FmtDefaults : 0x39E; -char FractEnable : 0xBF4; -char FScaleDisable : 0xA63; -short FSFCBLen : 0x3F6; -QHdr FSQHdr : 0x360; - -WindowPtr GhostWindow : 0xA84; -RgnHandle GrayRgn : 0x9EE; - -Ptr HeapEnd : 0x114; -char HiliteMode : 0x938; -RGBColor HiliteRGB : 0xDA0; -short HWCfgFlags : 0xB22; - -ProcPtr IAZNotify : 0x33C; -Ptr IWM : 0x1E0; - -ProcPtr JGNEFilter : 0x29A; - -short KeyRepThresh : 0x190; -short KeyThresh : 0x18E; -ProcPtr Key1Trans : 0x29E; -ProcPtr Key2Trans : 0x2A2; - -FamRec **LastFOND : 0xBC2; -long Lo3Bytes : 0x31A; -ProcPtr Lvl1DT[] : 0x192; -ProcPtr Lvl2DT[] : 0x1B2; - -GDHandle MainDevice : 0x8A4; -short MBarEnable : 0xA20; -short MBarHeight : 0xBAA; -ProcPtr MBarHook : 0xA2C; -short MemErr : 0x220; -Ptr MemTop : 0x108; -MCTableHandle MenuCInfo : 0xD50; -long MenuDisable : 0xB54; -short MenuFlash : 0xA24; -Handle MenuList : 0xA1C; -ProcPtr MenuHook : 0xA30; -long MinStack : 0x31E; - -short PaintWhite : 0x9DC; -short PrintErr : 0x944; - -char QDExist : 0x8F3; - -Ptr RAMBase : 0x2B2; -short ResErr : 0xA60; -ProcPtr ResErrProc : 0xAF2; -Boolean ResLoad : 0xA5E; -ProcPtr ResumeProc : 0xA8C; -long RndSeed : 0x156; -Ptr ROMBase : 0x2AE; -short RomMapInsert : 0xB9E; -short ROM85 : 0x28E; - -short SaveUpdate : 0x9DA; -RgnHandle SaveVisRgn : 0x9F2; -Ptr SCCRd : 0x1D8; -Ptr SCCWr : 0x1DC; -ScrapStuff ScrapInfo : 0x960; -char ScrDmpEnb : 0x2F8; -short ScreenRow : 0x106; -short ScrHRes : 0x104; -Ptr ScrnBase : 0x824; -short ScrVRes : 0x102; -char SdVolume : 0x260; -char SEvtEnb : 0x15C; -short SFSaveDisk : 0x214; -short SysEvtMask : 0x144; -short SysMap : 0xA58; -Handle SysMapHndl : 0xA54; -SysParmType SysParam : 0x1F8; -unsigned char SysResName[20] : 0xAD8; -short SysVersion : 0x15A; -THz SysZone : 0x2A6; - -Handle TEScrpHandle : 0xAB4; -short TEScrpLength : 0xAB0; -GDHandle TheGDevice : 0xCC8; -short TheMenu : 0xA26; -THz TheZone : 0x118; -volatile long Ticks : 0x16A; -volatile long Time : 0x20C; -volatile long TimeLM : 0x20C; -Handle TopMapHndl : 0xA50; -short TopMenuItem : 0xA0A; - -short UnitNtryCnt : 0x1D2; -DCtlHandle *UTableBase : 0x11C; - -volatile QHdr VBLQueue : 0x160; -QHdr VCBQHdr : 0x356; -Ptr VIA : 0x1D4; - -WidthTable **WidthTabHandle : 0xB2A; -WindowPeek WindowList : 0x9D6; -CGrafPtr WMgrCPort : 0xD2C; -GrafPtr WMgrPort : 0x9DE; -char WWExist : 0x8F2; - - -#endif // __LOMEM__ diff --git a/include/ThinkCIncludes/SANE.h b/include/ThinkCIncludes/SANE.h deleted file mode 100644 index cc31c43..0000000 --- a/include/ThinkCIncludes/SANE.h +++ /dev/null @@ -1,371 +0,0 @@ - -/* - * SANE.h - * - * Copyright (c) 1991 Symantec Corporation. All rights reserved. - * - * These interfaces are based on material copyrighted - * by Apple Computer, Inc., 1985-1991. - * - */ - -#ifndef __SANE__ -#define __SANE__ - -#ifndef __TYPES__ -#include -#endif - - - /* ---------- exceptions ---------- */ - - -#define INVALID 1 -#define UNDERFLOW 2 -#define OVERFLOW 4 -#define DIVBYZERO 8 -#define INEXACT 16 - -#define IEEEDEFAULTENV 0 - -typedef short exception, environment; - -typedef struct { - unsigned short haltexceptions; - unsigned short pendingCCR; - long pendingD0; -} mischaltinfo; - -typedef pascal void (*haltvector)(mischaltinfo *, void *src2, void *src, void *dst, short opcode); - - - /* ---------- formatting ---------- */ - - -#define SIGDIGLEN 20 -#define DECSTROUTLEN 80 - -enum { FLOATDECIMAL, FIXEDDECIMAL }; - -typedef struct { - char sgn; - short exp; - struct { - unsigned char length; - unsigned char text[SIGDIGLEN|1]; - } sig; -} decimal; - -typedef struct { - char style; - short digits; -} decform; - - - /* ---------- round/compare/classify ---------- */ - - -typedef enum { TONEAREST, UPWARD, DOWNWARD, TOWARDZERO } rounddir; -typedef enum { EXTPRECISION, DBLPRECISION, FLOATPRECISION } roundpre; - -typedef enum { - GREATERTHAN, LESSTHAN, EQUALTO, UNORDERED -} relop; - -typedef enum { - SNAN, QNAN, INFINITE, ZERONUM, NORMALNUM, DENORMALNUM -} numclass; - -/* low-level classify codes */ -enum { FCSNAN = 1, FCQNAN, FCINF, FCZERO, FCNORM, FCDENORM }; - -/* NAN codes */ -enum { - NANSQRT = 1, NANADD, - NANDIV = 4, - NANMUL = 8, NANREM, - NANASCBIN = 17, - NANCOMP = 20, NANZERO, - NANTRIG = 33, NANINVTRIG, - NANLOG = 36, NANPOWER, NANFINAN, - NANINIT = 255 -}; - - - /* ---------- high-level access ---------- */ - - -void procentry(environment *); -void procexit(environment); -void getenvironment(environment *); -void setenvironment(environment); -long testexception(exception); -void setexception(exception, long); -long testhalt(exception); -void sethalt(exception, long); -haltvector gethaltvector(void); -void sethaltvector(haltvector); -roundpre getprecision(void); -void setprecision(roundpre); -rounddir getround(void); -void setround(rounddir); - -extended nextextended(extended, extended); -extended nextdouble(extended, extended); -extended nextfloat(extended, extended); -numclass classextended(extended); -numclass classdouble(extended); -numclass classfloat(extended); -long signnum(extended); -relop relation(extended, extended); -extended copysign(extended, extended); - -void x80tox96(void *x80, void *x96); -void x96tox80(void *x96, void *x80); -void num2dec(const decform *, extended, decimal *); -extended dec2num(const decimal *); -void str2dec(const void *, short *, decimal *, Boolean *); -void cstr2dec(const void *, short *, decimal *, Boolean *); -void dec2str(const decform *, const decimal *, void *); -extended str2num(void *); -void num2str(decform *, extended, void *); - -extended annuity(extended, extended); -extended compound(extended, extended); -extended ipower(extended, short); -extended power(extended, extended); -extended randomx(extended *); -extended remainder(extended, extended, short *); -extended scalb(short, extended); - -extended nan(short); -#define inf() __inf -#define pi() __pi -extern extended __inf, __pi; - -extended _fp1(short, extended); -extended _elems1(short, extended); - -#ifndef _H_math -#define atan(x) _elems1(FATANX, x) -#define cos(x) _elems1(FCOSX, x) -#define exp(x) _elems1(FEXPX, x) -#define fabs(x) _fp1(FABSX, x) -#define log(x) _elems1(FLNX, x) -#define sin(x) _elems1(FSINX, x) -#define sqrt(x) _fp1(FSQRTX, x) -#define tan(x) _elems1(FTANX, x) -#endif - -#define exp1(x) _elems1(FEXP1X, x) -#define exp2(x) _elems1(FEXP2X, x) -#define log1(x) _elems1(FLN1X, x) -#define log2(x) _elems1(FLOG2X, x) -#define logb(x) _fp1(FLOGBX, x) -#define rint(x) _fp1(FRINTX, x) - - - /* ---------- low-level access ---------- */ - - -pascal void fp68k(...) = 0xA9EB; -pascal void elems68k(...) = 0xA9EC; -pascal void decstr68k(...) = 0xA9EE; - -enum { - - /* operand format masks */ - - FFEXT = 0x0000, - FFDBL = 0x0800, - FFSGL = 0x1000, - FFINT = 0x2000, - FFLNG = 0x2800, - FFCOMP = 0x3000, - - /* precision code masks */ - - FCEXT = 0x0000, - FCDBL = 0x4000, - FCSGL = (short) 0x8000, - - /* operation code masks (fp68k) */ - - FOADD = 0x0000, - FADDX = FOADD + FFEXT, - FADDD = FOADD + FFDBL, - FADDS = FOADD + FFSGL, - FADDI = FOADD + FFINT, - FADDL = FOADD + FFLNG, - FADDC = FOADD + FFCOMP, - FOSUB = 0x0002, - FSUBX = FOSUB + FFEXT, - FSUBD = FOSUB + FFDBL, - FSUBS = FOSUB + FFSGL, - FSUBI = FOSUB + FFINT, - FSUBL = FOSUB + FFLNG, - FSUBC = FOSUB + FFCOMP, - FOMUL = 0x0004, - FMULX = FOMUL + FFEXT, - FMULD = FOMUL + FFDBL, - FMULS = FOMUL + FFSGL, - FMULI = FOMUL + FFINT, - FMULL = FOMUL + FFLNG, - FMULC = FOMUL + FFCOMP, - FODIV = 0x0006, - FDIVX = FODIV + FFEXT, - FDIVD = FODIV + FFDBL, - FDIVS = FODIV + FFSGL, - FDIVI = FODIV + FFINT, - FDIVL = FODIV + FFLNG, - FDIVC = FODIV + FFCOMP, - FOSQRT = 0x0012, - FSQRTX = FOSQRT + FFEXT, - FOREM = 0x000C, - FREMX = FOREM + FFEXT, - FREMD = FOREM + FFDBL, - FREMS = FOREM + FFSGL, - FREMI = FOREM + FFINT, - FREML = FOREM + FFLNG, - FREMC = FOREM + FFCOMP, - FORTI = 0x0014, - FRINTX = FORTI + FFEXT, - FOTTI = 0x0016, - FTINTX = FOTTI + FFEXT, - FOSCALB = 0x0018, - FSCALBX = FOSCALB + FFEXT, - FOLOGB = 0x001A, - FLOGBX = FOLOGB + FFEXT, - FONEG = 0x000D, - FNEGX = FONEG + FFEXT, - FOABS = 0x000F, - FABSX = FOABS + FFEXT, - FOCPYSGN = 0x0011, - FCPYSGNX = FOCPYSGN + FFEXT, - FONEXT = 0x0013, - FNEXTX = FONEXT + FFEXT, - FNEXTD = FONEXT + FFDBL, - FNEXTS = FONEXT + FFSGL, - FOX2Z = 0x0010, - FX2X = FOX2Z + FFEXT, - FX2D = FOX2Z + FFDBL, - FX2S = FOX2Z + FFSGL, - FX2I = FOX2Z + FFINT, - FX2L = FOX2Z + FFLNG, - FX2C = FOX2Z + FFCOMP, - FOZ2X = 0x000E, -/* FX2X = FOZ2X + FFEXT, */ - FD2X = FOZ2X + FFDBL, - FS2X = FOZ2X + FFSGL, - FI2X = FOZ2X + FFINT, - FL2X = FOZ2X + FFLNG, - FC2X = FOZ2X + FFCOMP, - FOB2D = 0x000B, - FX2DEC = FOB2D + FFEXT, - FD2DEC = FOB2D + FFDBL, - FS2DEC = FOB2D + FFSGL, - FI2DEC = FOB2D + FFINT, - FL2DEC = FOB2D + FFLNG, - FC2DEC = FOB2D + FFCOMP, - FOD2B = 0x0009, - FDEC2X = FOD2B + FFEXT, - FDEC2D = FOD2B + FFDBL, - FDEC2S = FOD2B + FFSGL, - FDEC2I = FOD2B + FFINT, - FDEC2L = FOD2B + FFLNG, - FDEC2C = FOD2B + FFCOMP, - FOCMP = 0x0008, - FCMPX = FOCMP + FFEXT, - FCMPD = FOCMP + FFDBL, - FCMPS = FOCMP + FFSGL, - FCMPI = FOCMP + FFINT, - FCMPL = FOCMP + FFLNG, - FCMPC = FOCMP + FFCOMP, - FOCPX = 0x000A, - FCPXX = FOCPX + FFEXT, - FCPXD = FOCPX + FFDBL, - FCPXS = FOCPX + FFSGL, - FCPXI = FOCPX + FFINT, - FCPXL = FOCPX + FFLNG, - FCPXC = FOCPX + FFCOMP, - FOCLASS = 0x001C, - FCLASSX = FOCLASS + FFEXT, - FCLASSD = FOCLASS + FFDBL, - FCLASSS = FOCLASS + FFSGL, - FCLASSC = FOCLASS + FFCOMP, - FOGETENV = 0x0003, - FGETENV = FOGETENV, - FOSETENV = 0x0001, - FSETENV = FOSETENV, - FOTESTXCP = 0x001B, - FTESTXCP = FOTESTXCP, - FOSETXCP = 0x0015, - FSETXCP = FOSETXCP, - FOPROCENTRY = 0x0017, - FPROCENTRY = FOPROCENTRY, - FOPROCEXIT = 0x0019, - FPROCEXIT = FOPROCEXIT, - FOSETHV = 0x0005, - FSETHV = FOSETHV, - FOGETHV = 0x0007, - FGETHV = FOGETHV, - - /* operation code masks (elems68k) */ - - FOLNX = 0x0000, - FLNX = FOLNX, - FOLOG2X = 0x0002, - FLOG2X = FOLOG2X, - FOLN1X = 0x0004, - FLN1X = FOLN1X, - FOLOG21X = 0x0006, - FLOG21X = FOLOG21X, - FOEXPX = 0x0008, - FEXPX = FOEXPX, - FOEXP2X = 0x000A, - FEXP2X = FOEXP2X, - FOEXP1X = 0x000C, - FEXP1X = FOEXP1X, - FOEXP21X = 0x000E, - FEXP21X = FOEXP21X, - FOXPWRI = (short) 0x8010, - FXPWRI = FOXPWRI, - FOXPWRY = (short) 0x8012, - FXPWRY = FOXPWRY, - FOCOMPOUND = (short) 0xC014, - FCOMPOUND = FOCOMPOUND, - FOANNUITY = (short) 0xC016, - FANNUITY = FOANNUITY, - FOSINX = 0x0018, - FSINX = FOSINX, - FOCOSX = 0x001A, - FCOSX = FOCOSX, - FOTANX = 0x001C, - FTANX = FOTANX, - FOATANX = 0x001E, - FATANX = FOATANX, - FORANDX = 0x0020, - FRANDX = FORANDX, - - /* operation code masks (decstr68k) */ - - FOPSTR2DEC = 0x0002, - FPSTR2DEC = FOPSTR2DEC, - FOCSTR2DEC = 0x0004, - FCSTR2DEC = FOCSTR2DEC, - FODEC2STR = 0x0003, - FDEC2STR = FODEC2STR -}; - - - /* ---------- mixed-case interface ---------- */ - - -typedef decimal Decimal; -typedef decform DecForm; -#define Dec2Str dec2str -#define Str2Dec str2dec -#define CStr2Dec cstr2dec - - -#endif // __SANE__ diff --git a/include/ThinkCIncludes/SetUpA4.h b/include/ThinkCIncludes/SetUpA4.h deleted file mode 100644 index 2f4822b..0000000 --- a/include/ThinkCIncludes/SetUpA4.h +++ /dev/null @@ -1,42 +0,0 @@ - -/* - * SetUpA4.h - * - * Copyright (c) 1991 Symantec Corporation. All rights reserved. - * - * This defines "SetUpA4()" and "RestoreA4()" routines that will work - * in all A4-based projects. - * - * "RememberA4()" or "RememberA0()" must be called in advance to - * store away the value of A4 where it can be found by "SetUpA4()". - * The matching calls to "RememberA4()" (or "RememberA0()") and - * "SetUpA4()" *MUST* occur in the same file. - * - * Note that "RememberA4()", "RememberA0()" "SetUpA4()", and - * "RestoreA4()" are not external. Each file that uses them must - * include its own copy. - * - * If this file is used in the main file of a code resource with - * "Custom Headers", be sure to #include it *AFTER* the custom - * header! Otherwise, the code resource will begin with the code - * for the function "__GetA4()", defined below. - * - */ - - -static void -__GetA4(void) -{ - asm { - bsr.s @1 - dc.l 0 ; store A4 here -@1 move.l (sp)+,a1 - } -} - - -#define RememberA4() do { __GetA4(); asm { move.l a4,(a1) } } while (0) -#define RememberA0() do { __GetA4(); asm { move.l a0,(a1) } } while (0) - -#define SetUpA4() do { asm { move.l a4,-(sp) } __GetA4(); asm { move.l (a1),a4 } } while (0) -#define RestoreA4() do { asm { move.l (sp)+,a4 } } while (0) diff --git a/include/ThinkCIncludes/THINK.h b/include/ThinkCIncludes/THINK.h deleted file mode 100644 index ff281ac..0000000 --- a/include/ThinkCIncludes/THINK.h +++ /dev/null @@ -1,116 +0,0 @@ - -/* - * THINK.h - THINK C extensions to Apple headers - * - * Copyright (c) 1991 Symantec Corporation. All rights reserved. - * - * These interfaces are based on material copyrighted - * by Apple Computer, Inc., 1985-1991. - * - * This file contains material that has traditionally been part of - * the THINK C interfaces, but which does not appear in the Apple - * headers we are now using. It is provided for backward compatibility. - * - */ - -#ifndef __THINK__ -#define __THINK__ - -#ifndef __TYPES__ -#include -#endif - -#ifndef __FILES__ -#include -#endif - -#ifndef __QUICKDRAW__ -#include -#endif - - - /* SystemEdit arguments */ - -enum { - undoCmd, - cutCmd = 2, - copyCmd, - pasteCmd, - clearCmd -}; - - - /* dCtlFlags bits */ - -#define dNeedLock 0x4000 -#define dNeedTime 0x2000 -#define dNeedGoodBye 0x1000 -#define dStatEnable 0x0800 -#define dCtlEnable 0x0400 -#define dWritEnable 0x0200 -#define dReadEnable 0x0100 -#define drvrActive 0x0080 -#define dRAMBased 0x0040 -#define dOpened 0x0020 - - - /* I/O traps */ - -#define aRdCmd 2 -#define aWrCmd 3 -#define asyncTrpBit 0x0400 -#define noQueueBit 0x0200 - - - /* buttons */ - -enum { - OK = 1, - Cancel -}; - - - /* QuickDraw globals */ - -extern GrafPtr thePort; -extern Pattern white; -extern Pattern black; -extern Pattern gray; -extern Pattern ltGray; -extern Pattern dkGray; -extern Cursor arrow; -extern BitMap screenBits; -extern long randSeed; - - - /* Rect macros */ - -#define topLeft(r) (((Point *) &(r))[0]) -#define botRight(r) (((Point *) &(r))[1]) - - - /* Booleans */ - -#define TRUE 1 -#define FALSE 0 - - - /* param blocks */ - -typedef IOParam ioParam; -typedef FileParam fileParam; -typedef VolumeParam volumeParam; -typedef CntrlParam cntrlParam; - - - /* multi-segment non-applications */ - -void UnloadA4Seg(void *); - - - /* menu bar height (from Script.h) */ - -#define GetMBarHeight() (* (short*) 0x0BAA) - - -#endif // __THINK__ diff --git a/include/ThinkCIncludes/asm.h b/include/ThinkCIncludes/asm.h deleted file mode 100644 index 23694ed..0000000 --- a/include/ThinkCIncludes/asm.h +++ /dev/null @@ -1,141 +0,0 @@ - -/* - * asm.h - definitions useful with inline assembly - * - * Copyright (c) 1991 Symantec Corporation. All rights reserved. - * - * These interfaces are based on material copyrighted - * by Apple Computer, Inc., 1985-1991. - * - */ - -#ifndef __asm__ -#define __asm__ - - -/* trap modifier flags, e.g. "NewHandle SYS+CLEAR" */ - -enum { - /* Memory Manager traps */ - SYS = 2, /* applies to system heap */ - CLEAR = 1, /* zero allocated block */ - /* File Manager and Device Manager traps */ - ASYNC = 2, /* asynchronous I/O */ - HFS = 1, /* HFS version of trap (File Manager) */ - IMMED = 1, /* bypass driver queue (Device Manager) */ - /* string operations */ - MARKS = 1, /* ignore diacritical marks */ - CASE = 2, /* don't ignore case */ - /* GetTrapAddress, SetTrapAddress */ - NEWOS = 1, /* new trap numbering, OS trap */ - NEWTOOL = 3, /* new trap numbering, Toolbox trap */ - /* Toolbox traps */ - AUTOPOP = 2 /* return directly to caller's caller */ -}; - - -/* field offsets, e.g. "move.w d0,OFFSET(Rect,bottom)(a2)" */ - -#define OFFSET(type, field) ((int) &((type *) 0)->field) - - -/* - * additional trap definitions - * - * Two kinds of traps are defined here. - * - * * Traps that may be useful in inline assembly, or that have - * traditionally been available in inline assembly in THINK C, - * but for which the Apple headers do not supply inline definitions. - * - * * Traps for which the Apple headers supply inline definitions - * that, though fine in C, are inappropriate for use in inline - * assembly. (The inline assembler knows to prefer a trap defined - * with a leading underscore, regardless of whether an underscore - * is used when the trap is invoked.) - * - */ - -void _ADBOp(void) = 0xA07C; -void _AddDrive(void) = 0xA04E; - -void _Chain(void) = 0xA9F3; -void _CmpString(void) = 0xA03C; - -void _Date2Secs(void) = 0xA9C7; -void _DecStr68K(void) = 0xA9EE; -void _Delay(void) = 0xA03B; -void _DeleteUserIdentity(void) = { 0x700C, 0xA0DD }; - -void _Elems68K(void) = 0xA9EC; -void _EqualString(void) = 0xA03C; - -void _FindFolder(void) = { 0x7000, 0xA823 }; -void _FlushEvents(void) = 0xA032; -void _FP68K(void) = 0xA9EB; - -void _Gestalt(void) = 0xA1AD; -void _GetDefaultUser(void) = { 0x700D, 0xA0DD }; -void _GetHandleSize(void) = 0xA025; -void _GetItemStyle(void) = 0xA941; -void _GetOSEvent(void) = 0xA031; -void _GetPhysical(void) = { 0x7005, 0xA15C }; -void _GetPtrSize(void) = 0xA021; - -void _HandToHand(void) = 0xA9E1; -void _HCreateResFile(void) = 0xA81B; -void _HFSDispatch(void) = 0xA260; -void _HOpenResFile(void) = 0xA81A; - -void _InitZone(void) = 0xA019; -void _InternalWait(void) = 0xA07F; - -void _Launch(void) = 0xA9F2; -void _LwrString(void) = 0xA056; -void _LwrText(void) = 0xA056; - -void _MaxMem(void) = 0xA11D; - -void _NewGestalt(void) = 0xA3AD; -void _NMInstall(void) = 0xA05E; -void _NMRemove(void) = 0xA05F; - -void _OSDispatch(void) = 0xA88F; -void _OSEventAvail(void) = 0xA030; - -void _Pack0(void) = 0xA9E7; -void _Pack2(void) = 0xA9E9; -void _Pack3(void) = 0xA9EA; -void _Pack4(void) = 0xA9EB; -void _Pack5(void) = 0xA9EC; -void _Pack6(void) = 0xA9ED; -void _Pack7(void) = 0xA9EE; -void _Pack12(void) = 0xA82E; -void _PPostEvent(void) = 0xA12F; -void _PrGlue(void) = 0xA8FD; -void _PtrToHand(void) = 0xA9E3; -void _PurgeSpace(void) = 0xA162; - -void _RelString(void) = 0xA050; -void _ReplaceGestalt(void) = 0xA5AD; - -void _ScriptUtil(void) = 0xA8B5; -void _SCSIDispatch(void) = 0xA815; -void _SetFractEnable(void) = 0xA814; -void _ShutDown(void) = 0xA895; -void _SlotManager(void) = 0xA06E; -void _StartSecureSession(void) = { 0x700E, 0xA0DD }; -void _StripAddress(void) = 0xA055; -void _SwapMMUMode(void) = 0xA05D; -void _SysEnvirons(void) = 0xA090; -void _SysError(void) = 0xA9C9; - -void _TEDispatch(void) = 0xA83D; - -void _UprString(void) = 0xA054; -void _UprText(void) = 0xA054; - -void _WriteParam(void) = 0xA038; - - -#endif // __asm__ diff --git a/include/ThinkCIncludes/pascal.h b/include/ThinkCIncludes/pascal.h deleted file mode 100644 index 46715e3..0000000 --- a/include/ThinkCIncludes/pascal.h +++ /dev/null @@ -1,26 +0,0 @@ - -/* - * pascal.h - * - * Copyright (c) 1991 Symantec Corporation. All rights reserved. - * - */ - -#ifndef __pascal__ -#define __pascal__ - -// these functions modify their argument in place - don't use with constants -unsigned char *CtoPstr(char *); -char *PtoCstr(unsigned char *); -#define c2pstr(s) CtoPstr(s) -#define p2cstr(s) PtoCstr(s) -pascal unsigned char *C2PStr(char *); -pascal char *P2CStr(unsigned char *); - -// no longer needed - provided for backward compatibility -pascal void CallPascal (...) = { 0x205F, 0x4E90 }; -pascal char CallPascalB(...) = { 0x205F, 0x4E90 }; -pascal int CallPascalW(...) = { 0x205F, 0x4E90 }; -pascal long CallPascalL(...) = { 0x205F, 0x4E90 }; - -#endif // __pascal__ From 42b30553518c49867caa81867cb4167855b53012 Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Tue, 13 Apr 2021 08:56:19 +1000 Subject: [PATCH 02/10] refactor: use StringHandle for session data strings - It will come in handy to use StringHandle later because they are used by text utils and probably by the drawing functions - It also helps with memory fragmentation I suppose --- .vscode/c_cpp_properties.json | 5 ++- .vscode/settings.json | 9 +++-- Images/basilisk_ii_prefs | 2 +- macbrew-ui/macbrew.c | 11 ++----- macbrew-ui/mbConstants.h | 4 +-- macbrew-ui/mbDSessionList.c | 40 +++++++++++++++------- macbrew-ui/mbDataManager.c | 33 ++++++++++++------- macbrew-ui/mbListUtils.c | 62 +++++++++++++++++++++++++++++++++-- macbrew-ui/mbListUtils.h | 11 +++++++ macbrew-ui/mbMenus.c | 2 +- macbrew-ui/mbTypes.h | 6 ++-- macbrew-ui/mbWSplash.c | 5 +++ macbrew-ui/mbWSplash.h | 5 --- 13 files changed, 144 insertions(+), 51 deletions(-) diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 7326121..c11fe05 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -2,8 +2,7 @@ "env": { "myDefaultIncludePath": [ "${workspaceFolder}", - "${workspaceFolder}/include/CIncludes", - "${workspaceFolder}/include/ThinkCIncludes" + "${workspaceFolder}/include/CIncludes" ] }, "configurations": [ @@ -23,4 +22,4 @@ } ], "version": 4 -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index a19600c..fde07f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,6 +26,11 @@ "mbtypes.h": "c", "string": "cpp", "cstring": "c", - "mbdsessionlist.h": "c" - } + "mbdsessionlist.h": "c", + "events.h": "c", + "string_view": "c" + }, + "c-cpp-flylint.clang.enable": false, + "c-cpp-flylint.flexelint.enable": false, + "c-cpp-flylint.lizard.enable": false } \ No newline at end of file diff --git a/Images/basilisk_ii_prefs b/Images/basilisk_ii_prefs index f03fa6f..fec8159 100644 --- a/Images/basilisk_ii_prefs +++ b/Images/basilisk_ii_prefs @@ -2,7 +2,7 @@ displaycolordepth 0 disk ./system710-macbrew-dev-with-code.image extfs / screen win/512/384 -seriala /dev/pts/1 +seriala /dev/pts/3 serialb /dev/ttyS1 udptunnel false udpport 6066 diff --git a/macbrew-ui/macbrew.c b/macbrew-ui/macbrew.c index f01a55e..8c70eab 100644 --- a/macbrew-ui/macbrew.c +++ b/macbrew-ui/macbrew.c @@ -45,11 +45,10 @@ static void HandleMouseDown(EventRecord *theEvent) break; case inDrag: - if (windowKind == kSessionListWindowId) - { - DragWindow(theWindow, theEvent->where, &(*GetGrayRgn())->rgnBBox); - } + { + DragWindow(theWindow, theEvent->where, &(*GetGrayRgn())->rgnBBox); break; + } case inContent: if (theWindow != FrontWindow()) @@ -73,10 +72,6 @@ static void HandleMouseDown(EventRecord *theEvent) TrackGoAway(theWindow, theEvent->where)) { HideWindow(theWindow); - if (windowKind == kSessionListWindowId) - { - SessionListDialogDestroy(theWindow); - } } break; } diff --git a/macbrew-ui/mbConstants.h b/macbrew-ui/mbConstants.h index 9dd74af..dae0549 100644 --- a/macbrew-ui/mbConstants.h +++ b/macbrew-ui/mbConstants.h @@ -4,7 +4,7 @@ // WIND #define kSplashWindowId 128 -#define kSessionListWindowId 129 +#define kViewSessionWindowId 129 // PICT #define kSplashImageId 128 @@ -43,4 +43,4 @@ // TODO: Should be defined in MacWindows.h #define kDialogWindowKind 2 -#define kSessionListUserItem 3 \ No newline at end of file +#define kSessionListUserItem 3 diff --git a/macbrew-ui/mbDSessionList.c b/macbrew-ui/mbDSessionList.c index 1e56bc3..514cfb5 100644 --- a/macbrew-ui/mbDSessionList.c +++ b/macbrew-ui/mbDSessionList.c @@ -1,3 +1,4 @@ +#include #include #include "mbConstants.h" #include "mbUtil.h" @@ -11,6 +12,8 @@ typedef struct SessionListDialogState ListHandle listHandle; ControlHandle cancelButton; ControlHandle okButton; + ListItem **sessionListItems; + short sessionListItemCount; } SessionListDialogState; pascal Boolean SessionListEventFilterProc(DialogPtr theDialog, EventRecord *theEvent, short *itemHit); @@ -116,6 +119,7 @@ static void SetUpSessionListControl(DialogPtr parentDialog) // Create the list dialogState->listHandle = LNew(&visibleRect, &dataBounds, cellSize, 0, parentDialog, doDraw, noGrow, !includeScrollBar, includeScrollBar); + dialogState->sessionListItemCount = 0; SessionListDialogUnlockState(parentDialog); } @@ -193,6 +197,17 @@ DialogPtr SessionListDialogSetUp(void) void SessionListDialogDestroy(DialogPtr theDialog) { + // Clean up item data + short i; + SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); + + for (i = 0; i < dialogState->sessionListItemCount; i++) + { + DisposePtr((Ptr)dialogState->sessionListItems[i]); + } + + SessionListDialogUnlockState(theDialog); + // Controls DestroySessionListControl(theDialog); @@ -206,8 +221,7 @@ void SessionListDialogDestroy(DialogPtr theDialog) void SessionListDialogSetSessions(DialogPtr theDialog, Sequence *sessionReferences) { short i; - Cell cell = {0}; - const SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); + SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); const ListRec *sessionList = SessionListControlLock(dialogState); if (sessionList->dataBounds.bottom > 0) @@ -216,19 +230,24 @@ void SessionListDialogSetSessions(DialogPtr theDialog, Sequence *sessionReferenc Panic("\pCan only add rows to an empty list at this time"); } + dialogState->sessionListItems = (ListItem **)NewPtr(sessionReferences->size * sizeof(ListItem *)); + dialogState->sessionListItemCount = sessionReferences->size; + for (i = 0; i < sessionReferences->size; i++) { Handle sessionHandle = sessionReferences->elements[i]; - BrewSessionReference *brewSession = (BrewSessionReference *)*sessionHandle; + BrewSessionReference *brewSession; + ListItem *item; + Cell cell; - LAddRow(1, i, dialogState->listHandle); SetPt(&cell, 0, i); + HLock(sessionHandle); + brewSession = (BrewSessionReference *)*sessionHandle; + item = NewListItem(brewSession->name, cell); + HUnlock(sessionHandle); - LSetCell((unsigned char *)brewSession->name + 1, (char)*brewSession->name, cell, dialogState->listHandle); - if (i == 0) - { - LSetSelect(TRUE, cell, dialogState->listHandle); - } + AddListItem(dialogState->listHandle, item); + dialogState->sessionListItems[i] = item; } SessionListControlUnlock(dialogState); @@ -239,13 +258,10 @@ void SessionListDialogShow(DialogPtr theDialog) { short itemHit; ShowWindow(theDialog); - do { ModalDialog(&SessionListEventFilterProc, &itemHit); } while (itemHit != ok && itemHit != cancel); - - SessionListDialogDestroy(theDialog); } void SessionListControlHandleKeyboard(DialogPtr theDialog, char key) diff --git a/macbrew-ui/mbDataManager.c b/macbrew-ui/mbDataManager.c index 55b3632..50daa89 100644 --- a/macbrew-ui/mbDataManager.c +++ b/macbrew-ui/mbDataManager.c @@ -14,7 +14,7 @@ typedef struct ResponseReader static void InitReader(ResponseReader *reader, const SerialResponse *responseData); static void ReadBool(ResponseReader *reader, Boolean *outBoolean); static void ReadUnsignedShort(ResponseReader *reader, unsigned short *outShort); -static void ReadString(ResponseReader *reader, unsigned char *outString); +static void ReadString(ResponseReader *reader, StringHandle *outString); static void ReadSequence(ResponseReader *reader, Sequence *outSequence); static void ReadBrewSessionReference(ResponseReader *reader, Handle *outHandle); static void ValidateResponse(ResponseReader *reader); @@ -53,10 +53,11 @@ static void ReadUnsignedShort(ResponseReader *reader, unsigned short *outShort) *outShort = value; } -static void ReadString(ResponseReader *reader, unsigned char *outString) +static void ReadString(ResponseReader *reader, StringHandle *outString) { unsigned short length = 0; char *buffer = *(reader->response->data); + Str255 pString; ReadUnsignedShort(reader, &length); @@ -64,9 +65,11 @@ static void ReadString(ResponseReader *reader, unsigned char *outString) { Panic("\pCannot read strings larger than 254 characters at this time"); } - outString[0] = (char)length; + pString[0] = (char)length; - memcpy(outString + 1, (char *)buffer + reader->cursor, length); + memcpy(&pString[1], (char *)buffer + reader->cursor, length); + + *outString = NewString(pString); reader->cursor += length; } @@ -82,13 +85,18 @@ static void ReadSequence(ResponseReader *reader, Sequence *outSequence) static void ReadBrewSessionReference(ResponseReader *reader, Handle *outHandle) { - int s = sizeof(BrewSessionReference); - Handle handle = NewHandle(s); - BrewSessionReference *brewSessionReference = (BrewSessionReference *)*handle; + Handle handle = NewHandle(sizeof(BrewSessionReference)); + BrewSessionReference *brewSessionReference; + + HLock(handle); + + brewSessionReference = (BrewSessionReference *)*handle; + + ReadString(reader, &brewSessionReference->id); + ReadString(reader, &brewSessionReference->batch_code); + ReadString(reader, &brewSessionReference->name); - ReadString(reader, brewSessionReference->id); - ReadString(reader, brewSessionReference->batch_code); - ReadString(reader, brewSessionReference->name); + HUnlock(handle); *outHandle = handle; } @@ -96,8 +104,8 @@ static void ReadBrewSessionReference(ResponseReader *reader, Handle *outHandle) static void ValidateResponse(ResponseReader *reader) { Boolean success; - Str255 responseId; - ReadString(reader, responseId); + StringHandle responseId; + ReadString(reader, &responseId); ReadBool(reader, &success); // TODO: Check that response ID matches request ID @@ -149,3 +157,4 @@ void FetchBrewSessionReferences(Sequence **outSessionReferences) //DisposeResponse(&responseData); } +unsigned short red; diff --git a/macbrew-ui/mbListUtils.c b/macbrew-ui/mbListUtils.c index e56ec9a..19fc658 100644 --- a/macbrew-ui/mbListUtils.c +++ b/macbrew-ui/mbListUtils.c @@ -1,6 +1,11 @@ + +#include + #include "mbConstants.h" #include "mbListUtils.h" +static StringPtr CopyStringHandleToPtr(StringHandle sourceString); + void SelectCell(ListHandle listHandle, Cell oldCell, Cell newCell) { LSetSelect(FALSE, oldCell, listHandle); @@ -9,8 +14,8 @@ void SelectCell(ListHandle listHandle, Cell oldCell, Cell newCell) void MakeCellVisible(ListHandle listHandle, Cell newCell) { - ListRec *sessionList = *listHandle; - Rect visibleRect = sessionList->visible; + ListRec *list = *listHandle; + Rect visibleRect = list->visible; if (!PtInRect(newCell, &visibleRect)) { short newCol, newRow = 0; @@ -66,3 +71,56 @@ void DrawSessionListBorder(ListHandle listHandle) FrameRect(&border); SetPenState(&penState); } + +static StringPtr CopyStringHandleToPtr(StringHandle sourceString) +{ + StringPtr src, dest; + unsigned char length; + + HLock((Handle)sourceString); + src = *sourceString; + length = (unsigned char)*src; + dest = (StringPtr)NewPtr(length + 1); + memcpy(dest, src, length + 1); + HUnlock((Handle)sourceString); + + return dest; +} + +ListItem *NewListItem(StringHandle label, Cell cell) +{ + StringPtr copiedString; + ListItem *listItem = (ListItem *)NewPtr(sizeof(ListItem)); + + listItem->string = NULL; + listItem->cell = cell; + copiedString = CopyStringHandleToPtr(label); + listItem->string = copiedString; + listItem->stringData = (unsigned char *)copiedString + 1; + listItem->stringLength = (char)*copiedString; + + return listItem; +} + +void DestroyListItem(ListItem *listItem) +{ + if (listItem->string != NULL) + { + DisposePtr((Ptr)listItem->string); + listItem->string = NULL; + listItem->stringData = NULL; + listItem->stringLength = 0; + } + DisposePtr((Ptr)listItem); +} + +void AddListItem(ListHandle listHandle, ListItem *listItem) +{ + LAddRow(1, listItem->cell.v, listHandle); + + LSetCell(listItem->stringData, listItem->stringLength, listItem->cell, listHandle); + if (listItem->cell.v == 0) + { + LSetSelect(TRUE, listItem->cell, listHandle); + } +} diff --git a/macbrew-ui/mbListUtils.h b/macbrew-ui/mbListUtils.h index f9b533e..67df2cd 100644 --- a/macbrew-ui/mbListUtils.h +++ b/macbrew-ui/mbListUtils.h @@ -1,6 +1,17 @@ +typedef struct ListItem +{ + StringPtr string; + unsigned char *stringData; + unsigned char stringLength; + Cell cell; +} ListItem; + void SelectCell(ListHandle listHandle, Cell oldCell, Cell newCell); void MakeCellVisible(ListHandle listHandle, Cell newCell); Cell DetermineNewCellFromKey(ListHandle listHandle, Cell oldCellLoc, char keyHit); void DrawSessionListBorder(ListHandle listHandle); +ListItem *NewListItem(StringHandle label, Cell cell); +void DestroyListItem(ListItem *listItem); +void AddListItem(ListHandle listHandle, ListItem *listItemHandle); diff --git a/macbrew-ui/mbMenus.c b/macbrew-ui/mbMenus.c index 1273398..b6cd5f7 100644 --- a/macbrew-ui/mbMenus.c +++ b/macbrew-ui/mbMenus.c @@ -64,7 +64,7 @@ void HandleMenu(long mSelect) MakeCursorNormal(); SessionListDialogSetSessions(sessionListDialog, sessionReferences); SessionListDialogShow(sessionListDialog); - + SessionListDialogDestroy(sessionListDialog); break; } case quitItem: diff --git a/macbrew-ui/mbTypes.h b/macbrew-ui/mbTypes.h index 952a871..b33d9c7 100644 --- a/macbrew-ui/mbTypes.h +++ b/macbrew-ui/mbTypes.h @@ -1,8 +1,8 @@ typedef struct BrewSessionReference { - Str255 id; - Str255 batch_code; - Str255 name; + StringHandle id; + StringHandle batch_code; + StringHandle name; } BrewSessionReference; typedef struct Sequence diff --git a/macbrew-ui/mbWSplash.c b/macbrew-ui/mbWSplash.c index faf63d3..2ae42d6 100644 --- a/macbrew-ui/mbWSplash.c +++ b/macbrew-ui/mbWSplash.c @@ -1,6 +1,11 @@ #include "mbConstants.h" #include "mbWSplash.h" +typedef struct SplashWindowState +{ + PicHandle picHandle; +} SplashWindowState; + void SetUpSplashPic(WindowPtr parentWindow); void DestroySplachPic(WindowPtr parentWindow); diff --git a/macbrew-ui/mbWSplash.h b/macbrew-ui/mbWSplash.h index 79b0545..d2cff62 100644 --- a/macbrew-ui/mbWSplash.h +++ b/macbrew-ui/mbWSplash.h @@ -1,8 +1,3 @@ -typedef struct SplashWindowState -{ - PicHandle picHandle; -} SplashWindowState; - WindowPtr SetUpSplashWindow(void); void DestroySplashWindow(WindowPtr window); From 41c1340fdf9607c5932c6708d8348bc4688b01b2 Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Tue, 13 Apr 2021 08:56:42 +1000 Subject: [PATCH 03/10] feat: add skeleton for view session window --- macbrew-ui/mbWViewSession.c | 66 +++++++++++++++++++++++++++++++++++++ macbrew-ui/mbWViewSession.h | 4 +++ 2 files changed, 70 insertions(+) create mode 100644 macbrew-ui/mbWViewSession.c create mode 100644 macbrew-ui/mbWViewSession.h diff --git a/macbrew-ui/mbWViewSession.c b/macbrew-ui/mbWViewSession.c new file mode 100644 index 0000000..ba0826f --- /dev/null +++ b/macbrew-ui/mbWViewSession.c @@ -0,0 +1,66 @@ +#include "mbConstants.h" +#include "mbWViewSession.h" + +typedef struct ViewSessionWindowState +{ + StringHandle sessionId; +} ViewSessionWindowState; + +static void ViewSessionWindowInitState(WindowPtr theWindow); +static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow); +static void *ViewSessionWindowUnlockState(WindowPtr theWindow); + +static void ViewSessionWindowInitState(WindowPtr theWindow) +{ + const Handle viewSessionWindowStateHandle = NewHandleClear(sizeof(ViewSessionWindowState)); + SetWRefCon(theWindow, (long)viewSessionWindowStateHandle); + ((WindowPeek)theWindow)->windowKind = kViewSessionWindowId; +} + +static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow) +{ + const Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + + HLock(viewSessionWindowStateHandle); + return (ViewSessionWindowState *)*viewSessionWindowStateHandle; +} + +static void *ViewSessionWindowUnlockState(WindowPtr theWindow) +{ + const Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + + HUnlock(viewSessionWindowStateHandle); +} + +WindowPtr SessionViewWindowSetUp(void) +{ + WindowPtr viewSessionWindow = NULL; + viewSessionWindow = GetNewWindow(kViewSessionWindowId, viewSessionWindow, (WindowPtr)-1L); + + ViewSessionWindowInitState(viewSessionWindow); + + SetPort(viewSessionWindow); + + return viewSessionWindow; +} + +void SessionViewWindowDestroy(WindowPtr window) +{ + if (window != NULL) + { + DisposeWindow(window); + window = NULL; + } +} + +void SessionViewSetSessionId(WindowPtr window, StringHandle sessionId) +{ + // Temporary function to test session select + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + windowState->sessionId = sessionId; + + // Set title to demonstrate its working for now + SetWTitle(window, *sessionId); + + ViewSessionWindowUnlockState(window); +} diff --git a/macbrew-ui/mbWViewSession.h b/macbrew-ui/mbWViewSession.h new file mode 100644 index 0000000..48a811f --- /dev/null +++ b/macbrew-ui/mbWViewSession.h @@ -0,0 +1,4 @@ + +WindowPtr SessionViewWindowSetUp(void); +void SessionViewWindowDestroy(WindowPtr window); +void SessionViewSetSessionId(WindowPtr window, StringHandle sessionId); From 8dbd06942a7d27648133f4377aaf8cc3b1ccb66f Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Sat, 17 Apr 2021 15:25:37 +1000 Subject: [PATCH 04/10] feat: pass selected session to session window --- macbrew-ui/MacBrewUI.bin | Bin 148992 -> 12416 bytes macbrew-ui/mbDSessionList.c | 58 ++++++++++++++++++++---------------- macbrew-ui/mbDSessionList.h | 2 +- macbrew-ui/mbDataManager.c | 4 +-- macbrew-ui/mbMenus.c | 13 +++++++- macbrew-ui/mbSerial.c | 2 +- macbrew-ui/mbTypes.h | 2 ++ macbrew-ui/mbWViewSession.c | 21 ++++++++----- macbrew-ui/mbWViewSession.h | 4 ++- 9 files changed, 67 insertions(+), 39 deletions(-) diff --git a/macbrew-ui/MacBrewUI.bin b/macbrew-ui/MacBrewUI.bin index 17fccdf9b0fb1639cf338e626de30dff4432610e..d35218895f93e23b203a5a7f7a3070f7feaa83e4 100644 GIT binary patch literal 12416 zcmeHN4RBl4l|D~@mSrVTLTm&M$wdT@6O635ZYGm8Aj@{FIL?b>JHJB{$@Y`1tSzhh ziA|PW9Lj*h00~~o2Btrvr8H(Jn`J3Yy@5Ig(u4(SoR9^E0hVD&y2G}H2F7u{(SG;6 zC)t)0|1jI#?N08S`_8%No^$TG=iYnneOIE)_1;ETSZd$2mcCZ1sohYuuGC#Eq)8Xv z%QDwr`+oe{y0nU9aqr&m7IT~n-s~VMK-O$_oMKIFjj>iK7>%&TwpdFO3${gBgT&fG zQc!9dGgkw-35`2JK)Mu7sZrkCECnmPx6vC7v<2DVjuoda)fmg);aFy8tUi1P`*ti4 zjRt~#<_ovAGB1p3mcq;%3bh1OEzvd>2sVZ#Z$x6TU@Rgvu~wKzUil%yu9 zsel3X?)C;+ybUdqt-hH^J2c+9EBF-;T=BsFYY)sr?~Jq|;h+~1Z!0|&wi7w@MCG-c zYsnCPoru@Ub(TMI**ado<1iUoymClhtk7Y@5y2r-WOIg;+Jka!hD^GL-lInarqk7V zcS|zm2cR4V#ZcxAHcBlrt&8T;L3*{z$hnv|vRbBIN5^S}VDFR*vUpn!Y?7P}S4-X+Hgo`Yj5_bd1B_5OC5V3XQH8B}=c5kFLqu#w%T*t~Z_QoWtwKs;$b$7ISgHfrGmEClc4upvm zy&W5el)-)-Yzs;!H1@VR%-+Vz+Jd{Ka8wFw?BPsBZ>_XPZx2Vpjq_@yNLwu2D1j>! zkiyt7oUn)QejeoX#Jf3V61XIkAbU8HmS!T4i7LC(lP1fH)5`1g#D|0H@F1tBY2{(R z5kdxJPr5M5Gm*~%AD_A&4?XF^ zC{HU7|M9lD4?M`}X(sY!2=VMm7e;v|@_g*(AbXJ0(@f;~*v&=uqzj`w6Zz!Y390fm zfQ;B}B_#9=>3^p{gGPlpDlAfAvkJRZ__PXNQ($Jf0>{~sv?(2`&CeKIpZ)xnj&hm( z2AnioVoee&!SUTKd7Gqg*L=8NvRpyc02Sv3HhXL)hHiG>VR#Xs7s|3J!KoOk|HVtHo*to zlG_{X^pF-b2hl?XGnpuzN`JWgRyicg3N`sC55qELo8UGg)GXYL9p@_)oH7%C3GyQs z1@32_cu?etOY_G{+aR|Jl!uVflAJWKlRrF!5p~G#v05#>jOW%PJVfY^LK$D`=#`vw z*f{tjCrwa+n0HFjV_U#;&1qf~cHjlirrI?i=<~-!*J1zTnz_B`ubeVqJ%8jR5KyX@(UDodRm+$~KWj^@3U}$X=R9^U#0D=I(~fo9M`4RhC<0 zAsy>gmgAPJ9r}V?pXo@p zEo)~kR!6oz%Y(8RJYM1NbA9I1z#23|zy^}8l4a#rLQU_-pVwC->6Pza4y0;Bg2j0bfeNV77i&u0@s$z(70ID>MGj+$vLeJICw>(SkDoA zGlY|zvJIM)4X*VS;LdDqSdXxC2~I6+P^@)r;g=m*tqoi1LM`4%^JJ|$vL$)TIj^O) zp}bCtL~sjH9f(A^K$;!;G3|BS4pVDGofHmuTfiGoFET(*rpx%zwW%H)#@2>S(Lf7# z<1G9L-ltxEmy#LwUn~3kXkC3RE{z0ZBpSxW2<(ZF=1}uhk0dj=lALR|bK8xPz#eHQ z?`0&dm)bFJPq1_{r*4{2OgbV3lO8#A%UK>p6pOx#`67D+^QAuQ4MnJBT3@C{Ic*&N zPk8gm6~A>RGopsl%e=Y@WrR{QCf)D4DQ4H?nK+yEW4P>yMyK_3a`mUJyjiV&d)f>W zh^1CuDdxdEtUPbjyWSi0`lT>mcwc}0r|hlAe#aw4J(#TE9=}p;>uf`k##%)B1hNo=qD2sbVgAHesK*{5)??9$GLO zYD&eT@0b^Wlq1K{UzCbat zQ|~>qi(}YblRE;3|6RMnQ%&#oCG%JutJqv2q6uTgZvMN#CH9Z$c7=&!roVh_nl!po z`)u-loiV0+nptTa+sUf=_~Sgn&j5UfzC`TE92dVhH;-R91+@8-{L`MPQu|l>DP>kG z8^bVR{?GPQby+K#`C>>*WAj%&6^^aqOL$;by=a-#iz#)_z_!%hIC(5*Wf}IZKgGJ5 zFlO4ZnKy%z@VK=#xo=Z@=2;#BQ!D6_`_inV&V-)KcJ@te%jDHO?p!))HKS|k-~arC zK}KDxHxR^K%4P2+ZpSzFmG6HgV0EZV-lD=Q-~Z=Qw*!B^-~T>S{L1(LmG6J`ta`NW z&*1xCcqf}>{bR0Acpvvv&+FdG^<{dpHCaE;^=XDNmtR1Q0q_@qI&dDz^%<9<{7aNI z;Ohk+=l>^^ZwLJ-$_oKsM7bRBCESH=#ogGi0PE30pYQ={hJhW^e*?64z|#-Pc2Hi{ zegf&I!2Jqpo=45AfQPfsW*x`v*$I??jdC71PjF7b3hbo57}zdguW?Mn@7?}c!Jy}L z@Yg`@4L}p%o8TP;$8YrC1hxp+fPN(~53sj{Pjh|R&FJGGDE!v$lzvsNFKaol)0&Uq zmowR4X1$&3n^TbeSk@VoKS00#1^)N~@SUqE8wa^e{tY_$NU8>T0RAgDxjIZ$-+xo= zrDo)gf|9jj`Bt$vL#!N09M5|I?^p8fK~1OFt9?Q2H5!0_f&3D3p3Cp~Cq;qd^ zcaUIPop@U8)$b60ZY9=AE#iTm^PoNMH-hTsR57-3*t8==Vz2JF-%xeHuZgb}V{DbF z!#B5}WcvoOLhLmRGPB2!=VUh#L;LQ$PBB(cQeIkLin}ZEzi;1uFu$M9zt4yMF2pkeO}h`J%`Q z3x2nB>)XeQtu|}$R==&b)V}~ZZ*wVrVtN)B5iBaoPoVFb(Nnc4{Pl78;Hjq*7rEV< z?xJO6t_qOJ$Ld|XEVV9QN2syqB;YXT2gf6WJv3LvpW(RKl^FFmv^IlB$LVEXS~V|i z^u2EO-d5r}4ywVli>^*wEV_Pcscp~bz~IB(RpZ)j9(+(~UA3_|Sw>s=Tz|5hgsT1_ zS^vL7w>B|abp7pIcXaUKLG+}%>YJsBi^WI9Z=<*HknmB(msWcFonouM2wqz1pIck% zTd1@roby;!tk*MF#ri#1o5IIvu^XeoHJsrVvps*j$X5UHV$~tc+Wew?%Ja_SuE-riO;dRvL>ng!?RmZH&~p^c@!&oIVLdHy+l$7LP8(9s)CL`l|j?MwM| zi*MmdXmh{pH=q8_bB9PL>1_G9K^c=gjkX_UH6crP=coG|$3DHET%F|ThrXIH6qKV9=&m*0T(Uf^NoRiqON zC0??57NDe2OID8#w8dp3iGEv`7>mCs#?B98Htivev5&OkUlC?K)_6(ThVqKrs3*>L z)?nQ;qZrHIGtzCV{@c_4w)fp*r%ub|J^iEPv=v*esJnac5t-&wX~yo}kzF#SUZq$D zdq?VJic6*F413WD3!lx2AEo9yMm%xEqr>|QpLOu;&LV5E_3+j$Cle!8F8@Vwh>fbW zgX3r;D$Nx%9Vl%z3^+@NNaL_@ej-*#y24V@*$T;6xD&DcYzZx;Ji+aH<>h=@F1RaB zyn4Fa?Q*&Ljt$&h{UMdumZ0THQY#SU>3gVuV2b)XnJ^7U!8G;eF%4CnyB1aL-DNKC zEZ>W^8Q!sS>|B&1Q(~wy$u$O0V|M(OmqxKxa;G6;2Q2c)^}f#w!hD=byvRG=dK*QPcnAj z-Js*`*Ev0({0;FP0dvK0TL$LI5+3F9oG54Y{3LG)%gWc*R{p-SwsLibCuagzS8b)? z9-_N0ZrWc+Jx@87Ao}Zl@AG-~n~KLPeT$ej?u3>fv8$kSJFxEjI<_vpEF@q=bWbd1 zM$ldP?i9~TwvK5%)(NfHYAczO!5ep7EHom@SHIgAxA**b;~R!OmLp|X(+7I^kB@GpQL1^y)P&w<|${2=f@0Dp4ao@rtEl;bzyy~w{%9HA)q zLf|_Oz9`B~D0|d$80C6c(}p?7FF1@?&%3cRl}y-~3LxQw#1Nnh@N>Y`fc%6~1o#KQ z<$%~n$i{YxF^BNPnx*cL`H962AztYi6@ST2;(dX=rqtP*$rxYa5ZKyyr5NLJgD3cM zb>h5V$DFPMoXW2l#{Mr&+t;7V*bdSnjh*JZOY7XgyZKoL@CWN~paOh)OEv!YK=cAX zUjg>>sDa;y-{j{SXw2a!7(g38xd7Vhwyo#o)n($EO415zl=Hi=yWCZWU(JNO%jI7> zg@^d*0riiUyEbvY*XpX}`jItT*5bZaV~~wSgRnBh=R1ysgTOrsuG0a>_dlm=&jXJt zxM2nGCI!!o0l%FzoG0^L{5Rqq(#iP00w2Im#_7KWUQc=%{~Y`WNJu0WlI;v|rs&b% z4;+1$c`g92QStu-jy`d^NdWGoERO5(ACdLV<=^1{0?GEgZ*H9fB{4F#>E!I4pG>W6SbCp*Y5|4RKN$uCOH;W6P@l zB!u-#6qW*MY3$N`Y073(C(R~SHeo5vo457L=5g38p?R`dhtQPprj)W-!V*F$b*bYT z-|u(kUj4CwH0_u7dB5*_RxtOTduHa$%$YN1X3qREn{}tR+12HHV9hG_7yat$S9|U# zbJu95$JSQninH#_`ry0fUrhIieLei}Q?uv;5w)%!rY-9MaKQ1d*wWeD+u;lLbc@ZM zz3nYxu(L;6CH0`FD8U_89@lYvrHtk9wCzfzRj<^>7>D{BYng|Ap}EhUT1=M_$fRCu~ zKK1>Nc>hR+e~kCbDxC1r{HOAvG2{Ic6+Vae|AhM|xL1+CkKxWMe)QCQg2!vhw}x5@ z24|f^3U&4`mWyAde?o+r4;Ei z`wHk3;av=e`qfs!xs`Alz|o(!1`yJspSBip5Jq#S@=Z?-aN5;yy8ze0sq&|}2RO*3 zwgG`CU6N1yG>}298b%A*)P4=F9UcZaJzFh1*emkSDI~%|W*sPdMg7`(rt5l(G4kbJ zrvJ^~=0y6A{Y=;HC6&zPD|kRZ(KtAUXXm2g`vmt*;i&EG1bbW~3POYTejmp?18nJ-+UqW{3#=>Vazk zF1ey>qe`Cr9h=KuVuwshh?_zAA@(m}`r_OwtE;QIv#i`j2r3HaIHZe_V=QxKE*XpE z5>>=we4jrN*b=5F(|&rv=I!oCX>_luy+bV5+M1(alC3#pYMZo&!65tmS;C%`3fdu%K1b8?Bel-C;lmH)2fZvS6 zhV}$FlmPc6z?&1`z63a&0B=u#cO}5P6X1aa_^|}|i3Iq`1b8q3el`IfN`PNXfcGcB zFRL(PPckiIx*{1JG8#7EFC8Kn3Xk9;3Gmwq@JSAj@h~4Iz~>U+O9}8q95!n?{9B4^ zF5vJVIQ%4s&k}qSc#YpAW3$#m21vPF@5lXIn+=}7d%Jd+wZd)AD_&Jw(NsH}Qd>PC zAI{y4=j*xK;S;VMPI{$wSg!+o3GP+6Tvx!28gzFu!F{)TGt*d#Y1A1WHgsttpYus>Oh}8}YhXXp#KtMaVs#<`v(&y0)@*Sa(!3 zO1fO9xRi-V-<=z-?aeE$C~GW(-z#E`W%De(C=A+=%QpBsa<}IdH#F8a!1oD!&&%D7 z(Djuy_}(Sfh(f?vJFML&guw=*f2?*QWqIvd`9#_HER$t*NF20FcP9159_v}1byO`Dfi$;^nxjVVX$RzF>q%rooxBn9VeZj5!cpZZfBBJxllI6Jrm@xrzr)b@gZfLSW>jepHU zO%$b={OI1r&D!Te#?gWOKheJ6x1ikT2N#NU7PrML8OqKweUE0ldGyd-Kac8xyb z*Vgu0*r1KViJHM2OWo?<$Rpxv*APozwh?MJbMQvj^X`qzD6WoNDlj7l7jl|@{@QZ0 z#T+aNSnA6H>2SGg%L0t;1H>3|RA7}+cJ1W5^)dLZ1o-H?FUV7rZ|$Cf#mwm0$P9k5 z(bZ;HS=6hV_pD@$AXPNNx zdkQ8?i(Ow%Kg(7YOhzu=RpvjBI(BmTbNUZ+8pEJ5+$HxgPABVCD_iN$SOIG8e-0Rr z{mp?PCin!4l?e$kk0I^H#k!DbPx#Cuj@Qn7lexmoaR~J4x>$kPVb(eyl0SL`yb+hr zmC`MXnXqS9!E2P8fs3qB4!YseyvwmJBoJ19L0nG^usEP-cdaCgXgOb>?z|Ak8kPlgQ)%FhlQ*H0qIHYL{MxrAusrIiiD(poYFD_qQQCZ3&gW~!+w0lus z+iTr2vBy&L-DCgp@cXa5dyIc?IW)WYz+6tZv$prZ&J9^M z4ina0UBcYekG|$eaaR|l)b$v?U;Q| zt?9S3$a9WtX#ZsY2P9|zR=Ky@KTk{=bb`t=;timAFQ7fv2Jw!;#UTx9M0k3>s0ZF< zb;rW25bqF^B=fXXEcMl*Qv^32nzHMm)3h@3N z-b?UCKf)~H`r2NH=IN>&cgXnke1~SR+A&%CQ}Jhbek#7H_*q(+elXji5myaX*OEbi z|G>*4c`P7^PS*g(Y8fV|Y{88$-@Unlhqwinp7!1rpI8j@ncL@W@pW~V%z+n$qhXmw zB7hIp6@Z?~{DXh%jFijP?&rDJGiY))=G{#>G*kUk~~``OzRJ#FNsE(~q?Pe|qiNTLyfbHwA5 z^+kI?&C>*9sYjSqqm&?&bWppT@ᦡ^1SwRtFz`r>(H>si!& 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 } - if (windowCode == inContent && windowKind == kDialogWindowKind) + if (windowCode == inContent && windowKind == kDialogWindowKind && SessionListControlMouseDown(theDialog, *theEvent)) { - SessionListControlMouseDown(theDialog, *theEvent); + *itemHit = ok; + return TRUE; } } else @@ -126,8 +128,16 @@ static void SetUpSessionListControl(DialogPtr parentDialog) static void DestroySessionListControl(DialogPtr parentDialog) { + short i; SessionListDialogState *dialogState = SessionListDialogLockState(parentDialog); + for (i = 0; i < dialogState->sessionListItemCount; i++) + { + ListItem *item = dialogState->sessionListItems[i]; + DestroyListItem(item); + } + dialogState->sessionListItemCount = 0; + if (dialogState->listHandle != NULL) { LDispose(dialogState->listHandle); @@ -222,9 +232,8 @@ void SessionListDialogSetSessions(DialogPtr theDialog, Sequence *sessionReferenc { short i; SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); - const ListRec *sessionList = SessionListControlLock(dialogState); - if (sessionList->dataBounds.bottom > 0) + if (dialogState->sessionListItemCount > 0) { // TODO: Delete existing rows Panic("\pCan only add rows to an empty list at this time"); @@ -254,20 +263,32 @@ void SessionListDialogSetSessions(DialogPtr theDialog, Sequence *sessionReferenc SessionListDialogUnlockState(theDialog); } -void SessionListDialogShow(DialogPtr theDialog) +short SessionListDialogShow(DialogPtr theDialog) { short itemHit; ShowWindow(theDialog); + do { ModalDialog(&SessionListEventFilterProc, &itemHit); } while (itemHit != ok && itemHit != cancel); + + return SessionListGetSelectedCell(theDialog).v; +} + +Cell SessionListGetSelectedCell(DialogPtr theDialog) +{ + const SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); + Cell cell = {0, 0}; + LGetSelect(TRUE, &cell, dialogState->listHandle); + SessionListDialogUnlockState(theDialog); + return cell; } void SessionListControlHandleKeyboard(DialogPtr theDialog, char key) { const SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); - Cell newCell = {0}, oldCell = {0}; + Cell newCell = {0, 0}, oldCell = {0, 0}; if (dialogState->listHandle == NULL) { @@ -300,36 +321,23 @@ static void SessionListControlUpdate(DialogPtr theDialog) } // See: http://mirror.informatimago.com/next/developer.apple.com/documentation/mac/MoreToolbox/MoreToolbox-212.html#HEADING212-0 -static void SessionListControlMouseDown(DialogPtr theDialog, EventRecord theEvent) +static Boolean SessionListControlMouseDown(DialogPtr theDialog, EventRecord theEvent) { + Boolean itemSelected = false; const SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); const ListRec *sessionList = SessionListControlLock(dialogState); ControlHandle selectedControl; - short selectedControlCode; SetPort(sessionList->port); GlobalToLocal(&theEvent.where); - selectedControlCode = FindControl(theEvent.where, theDialog, &selectedControl); - if (LClick(theEvent.where, theEvent.modifiers, dialogState->listHandle)) { - // Double click - Select session - } - - switch (selectedControlCode) - { - case inButton: - { - - if (TrackControl(selectedControl, theEvent.where, NULL)) - { - // Single click - Select session or go away - } - break; - } + // Double clicked + itemSelected = TRUE; } SessionListControlUnlock(dialogState); SessionListDialogUnlockState(theDialog); + return itemSelected; } diff --git a/macbrew-ui/mbDSessionList.h b/macbrew-ui/mbDSessionList.h index bce4728..d13be1b 100644 --- a/macbrew-ui/mbDSessionList.h +++ b/macbrew-ui/mbDSessionList.h @@ -4,4 +4,4 @@ struct Sequence; DialogPtr SessionListDialogSetUp(void); void SessionListDialogDestroy(DialogPtr window); void SessionListDialogSetSessions(DialogPtr window, struct Sequence *sessionReferences); -void SessionListDialogShow(DialogPtr theDialog); +short SessionListDialogShow(DialogPtr theDialog); diff --git a/macbrew-ui/mbDataManager.c b/macbrew-ui/mbDataManager.c index 50daa89..1eb3900 100644 --- a/macbrew-ui/mbDataManager.c +++ b/macbrew-ui/mbDataManager.c @@ -153,8 +153,6 @@ void FetchBrewSessionReferences(Sequence **outSessionReferences) *outSessionReferences = sessionReference; - // TODO: Disposing! - - //DisposeResponse(&responseData); + DisposeResponse(&responseData); } unsigned short red; diff --git a/macbrew-ui/mbMenus.c b/macbrew-ui/mbMenus.c index b6cd5f7..a428019 100644 --- a/macbrew-ui/mbMenus.c +++ b/macbrew-ui/mbMenus.c @@ -4,6 +4,7 @@ #include "mbTypes.h" #include "mbDSessionList.h" #include "mbUtil.h" +#include "mbWViewSession.h" MenuHandle appleMenu, fileMenu; @@ -59,12 +60,22 @@ void HandleMenu(long mSelect) case listSessionsItem: { WindowPtr sessionListDialog = SessionListDialogSetUp(); + short selectedItem; + BrewSessionReferenceHandle selectedSession; + WindowPtr viewSessionWindow; MakeCursorBusy(); FetchBrewSessionReferences(&sessionReferences); MakeCursorNormal(); SessionListDialogSetSessions(sessionListDialog, sessionReferences); - SessionListDialogShow(sessionListDialog); + selectedItem = SessionListDialogShow(sessionListDialog); + if (selectedItem < 0 || selectedItem > sessionReferences->size - 1) + { + Panic("\pSelected item out of range!"); + } SessionListDialogDestroy(sessionListDialog); + selectedSession = (BrewSessionReferenceHandle)sessionReferences->elements[selectedItem]; + viewSessionWindow = SessionViewWindowSetUp(); + SessionViewSetSession(viewSessionWindow, selectedSession); break; } case quitItem: diff --git a/macbrew-ui/mbSerial.c b/macbrew-ui/mbSerial.c index 70c06a9..0c799b5 100644 --- a/macbrew-ui/mbSerial.c +++ b/macbrew-ui/mbSerial.c @@ -12,7 +12,7 @@ // as many bytes as it sends to cancel out the echo // // Note: This needs to be set to zero when emulating in Basilisk II because there is no fake echo there -#define SUPRESS_ECHO 1 +#define SUPRESS_ECHO 0 #define kChecksumBytes 4 // Accounts for \r\n on every response #define kSuffixSize 2 diff --git a/macbrew-ui/mbTypes.h b/macbrew-ui/mbTypes.h index b33d9c7..3c9362e 100644 --- a/macbrew-ui/mbTypes.h +++ b/macbrew-ui/mbTypes.h @@ -5,6 +5,8 @@ typedef struct BrewSessionReference StringHandle name; } BrewSessionReference; +typedef BrewSessionReference **BrewSessionReferenceHandle; + typedef struct Sequence { unsigned short size; diff --git a/macbrew-ui/mbWViewSession.c b/macbrew-ui/mbWViewSession.c index ba0826f..3fcfeb8 100644 --- a/macbrew-ui/mbWViewSession.c +++ b/macbrew-ui/mbWViewSession.c @@ -1,9 +1,10 @@ #include "mbConstants.h" #include "mbWViewSession.h" +#include "mbTypes.h" typedef struct ViewSessionWindowState { - StringHandle sessionId; + BrewSessionReferenceHandle brewSessionReferenceHandle; } ViewSessionWindowState; static void ViewSessionWindowInitState(WindowPtr theWindow); @@ -12,14 +13,14 @@ static void *ViewSessionWindowUnlockState(WindowPtr theWindow); static void ViewSessionWindowInitState(WindowPtr theWindow) { - const Handle viewSessionWindowStateHandle = NewHandleClear(sizeof(ViewSessionWindowState)); + Handle viewSessionWindowStateHandle = NewHandleClear(sizeof(ViewSessionWindowState)); SetWRefCon(theWindow, (long)viewSessionWindowStateHandle); ((WindowPeek)theWindow)->windowKind = kViewSessionWindowId; } static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow) { - const Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); HLock(viewSessionWindowStateHandle); return (ViewSessionWindowState *)*viewSessionWindowStateHandle; @@ -27,7 +28,7 @@ static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow) static void *ViewSessionWindowUnlockState(WindowPtr theWindow) { - const Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); HUnlock(viewSessionWindowStateHandle); } @@ -53,14 +54,20 @@ void SessionViewWindowDestroy(WindowPtr window) } } -void SessionViewSetSessionId(WindowPtr window, StringHandle sessionId) +void SessionViewSetSession(WindowPtr window, BrewSessionReferenceHandle brewSessionReferenceHandle) { // Temporary function to test session select ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); - windowState->sessionId = sessionId; + BrewSessionReference *brewSessionReference = NULL; + windowState->brewSessionReferenceHandle = brewSessionReferenceHandle; + HLock((Handle)brewSessionReferenceHandle); + brewSessionReference = *brewSessionReferenceHandle; + HLock((Handle)brewSessionReference->name); // Set title to demonstrate its working for now - SetWTitle(window, *sessionId); + SetWTitle(window, *(brewSessionReference->name)); + HUnlock((Handle)brewSessionReference->name); + HUnlock((Handle)brewSessionReferenceHandle); ViewSessionWindowUnlockState(window); } diff --git a/macbrew-ui/mbWViewSession.h b/macbrew-ui/mbWViewSession.h index 48a811f..7c072f5 100644 --- a/macbrew-ui/mbWViewSession.h +++ b/macbrew-ui/mbWViewSession.h @@ -1,4 +1,6 @@ +struct BrewSessionReference; + WindowPtr SessionViewWindowSetUp(void); void SessionViewWindowDestroy(WindowPtr window); -void SessionViewSetSessionId(WindowPtr window, StringHandle sessionId); +void SessionViewSetSession(WindowPtr window, struct BrewSessionReference **brewSessionReferenceHandle); From 078c90b1d1910edfb11521337a95d3aaf64f9e59 Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Sun, 29 Aug 2021 10:44:59 +1000 Subject: [PATCH 05/10] feat: load session data from proxy --- .vscode/settings.json | 8 +++++- macbrew-proxy/src/main.rs | 2 +- macbrew-ui/mbDataManager.c | 51 ++++++++++++++++++++++++++++++++++++- macbrew-ui/mbDataManager.h | 2 ++ macbrew-ui/mbMenus.c | 7 ++++- macbrew-ui/mbTypes.h | 49 +++++++++++++++++++++++++++++++---- macbrew-ui/mbUtil.c | 7 +++++ macbrew-ui/mbUtil.h | 1 + macbrew-ui/mbWViewSession.c | 29 +++++++++++++-------- macbrew-ui/mbWViewSession.h | 4 +-- 10 files changed, 139 insertions(+), 21 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fde07f2..8a92793 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,7 +28,13 @@ "cstring": "c", "mbdsessionlist.h": "c", "events.h": "c", - "string_view": "c" + "string_view": "c", + "mbwviewsession.h": "c", + "*.tcc": "c", + "cstdio": "c", + "string.h": "c", + "stdio.h": "c", + "textutils.h": "c" }, "c-cpp-flylint.clang.enable": false, "c-cpp-flylint.flexelint.enable": false, diff --git a/macbrew-proxy/src/main.rs b/macbrew-proxy/src/main.rs index 02f6e64..5ab8822 100644 --- a/macbrew-proxy/src/main.rs +++ b/macbrew-proxy/src/main.rs @@ -32,7 +32,7 @@ struct Opts { } async fn handle_line(line: &str) -> Result> { - println!("Received command"); + println!("Received command: [{}]", line); let components = line.split_terminator(' ').collect::>(); if let Some((request_id, rest)) = components.split_first() { let response = match rest { diff --git a/macbrew-ui/mbDataManager.c b/macbrew-ui/mbDataManager.c index 1eb3900..9cb195a 100644 --- a/macbrew-ui/mbDataManager.c +++ b/macbrew-ui/mbDataManager.c @@ -1,3 +1,4 @@ +#include #include #include "mbTypes.h" @@ -18,6 +19,8 @@ static void ReadString(ResponseReader *reader, StringHandle *outString); static void ReadSequence(ResponseReader *reader, Sequence *outSequence); static void ReadBrewSessionReference(ResponseReader *reader, Handle *outHandle); static void ValidateResponse(ResponseReader *reader); +static void AssertReaderEnd(const ResponseReader *reader, const SerialResponse *responseData); +static char *BuildCommand(const char *formatString, const unsigned char *arg); // Response Structure // ----------------- @@ -116,6 +119,15 @@ static void ValidateResponse(ResponseReader *reader) } } +static void AssertReaderEnd(const ResponseReader *reader, const SerialResponse *responseData) +{ + Boolean success = reader->cursor == responseData->length; + if (!success) + { + Panic("\pCursor is not at the end of the response. There may be an issue with the deserializer."); + } +} + void Ping() { SerialResponse *responseData; @@ -151,8 +163,45 @@ void FetchBrewSessionReferences(Sequence **outSessionReferences) ReadBrewSessionReference(&reader, &sessionReference->elements[i]); } + AssertReaderEnd(&reader, responseData); + *outSessionReferences = sessionReference; DisposeResponse(&responseData); } -unsigned short red; + +void FetchBrewSession(StringHandle sessionId, BrewSession **outBrewSession) +{ + Str255 command; + Str255 cSessionId; + SerialResponse *responseData; + ResponseReader reader; + BrewSession *brewSession = (BrewSession *)NewPtr(sizeof(BrewSession)); + + HLock((Handle)sessionId); + PascalToCStringCopy(cSessionId, *sessionId); + HUnlock((Handle)sessionId); + + sprintf((char *)command, "1 GET SESSION %s\r", cSessionId); + + SetUpSerial(); + SendCommand((char *)command); + ReadResponse(&responseData); + TearDownSerial(); + + InitReader(&reader, responseData); + + ValidateResponse(&reader); + + ReadString(&reader, &brewSession->id); + ReadString(&reader, &brewSession->phase); + ReadString(&reader, &brewSession->batch_code); + ReadString(&reader, &brewSession->recipe_title); + ReadString(&reader, &brewSession->recipe_id); + + AssertReaderEnd(&reader, responseData); + + *outBrewSession = brewSession; + + DisposeResponse(&responseData); +} diff --git a/macbrew-ui/mbDataManager.h b/macbrew-ui/mbDataManager.h index d40ddbc..f69a25a 100644 --- a/macbrew-ui/mbDataManager.h +++ b/macbrew-ui/mbDataManager.h @@ -1,5 +1,7 @@ struct Sequence; struct SerialResponse; +struct BrewSession; void Ping(void); void FetchBrewSessionReferences(struct Sequence **outSessionReferences); +void FetchBrewSession(StringHandle sessionId, struct BrewSession **outBrewSession); diff --git a/macbrew-ui/mbMenus.c b/macbrew-ui/mbMenus.c index a428019..8f1511a 100644 --- a/macbrew-ui/mbMenus.c +++ b/macbrew-ui/mbMenus.c @@ -62,6 +62,7 @@ void HandleMenu(long mSelect) WindowPtr sessionListDialog = SessionListDialogSetUp(); short selectedItem; BrewSessionReferenceHandle selectedSession; + BrewSessionHandle brewSession; WindowPtr viewSessionWindow; MakeCursorBusy(); FetchBrewSessionReferences(&sessionReferences); @@ -74,8 +75,12 @@ void HandleMenu(long mSelect) } SessionListDialogDestroy(sessionListDialog); selectedSession = (BrewSessionReferenceHandle)sessionReferences->elements[selectedItem]; + HLock((Handle)selectedSession); + FetchBrewSession((*selectedSession)->id, brewSession); + HUnlock((Handle)selectedSession); + viewSessionWindow = SessionViewWindowSetUp(); - SessionViewSetSession(viewSessionWindow, selectedSession); + SessionViewSetSession(viewSessionWindow, brewSession); break; } case quitItem: diff --git a/macbrew-ui/mbTypes.h b/macbrew-ui/mbTypes.h index 3c9362e..b1b6041 100644 --- a/macbrew-ui/mbTypes.h +++ b/macbrew-ui/mbTypes.h @@ -1,3 +1,9 @@ +typedef struct Sequence +{ + unsigned short size; + Handle *elements; +} Sequence; + typedef struct BrewSessionReference { StringHandle id; @@ -5,10 +11,43 @@ typedef struct BrewSessionReference StringHandle name; } BrewSessionReference; -typedef BrewSessionReference **BrewSessionReferenceHandle; +typedef struct BrewSession +{ + StringHandle id; + StringHandle phase; + StringHandle batch_code; + StringHandle recipe_title; + StringHandle recipe_id; +} BrewSession; -typedef struct Sequence +typedef struct Fermentable { - unsigned short size; - Handle *elements; -} Sequence; + StringHandle name; + StringHandle amount; +} Fermentable; + +typedef struct Hop +{ + StringHandle name; + StringHandle amount; + StringHandle time; +} Hop; + +typedef struct Yeast +{ + StringHandle name; + StringHandle amount; +} Yeast; + +typedef struct Recipe +{ + StringHandle name; + StringHandle version; + StringHandle recipe_type; + Sequence *fermentables; + Sequence *hops; + Sequence *yeast; +} Recipe; + +typedef BrewSessionReference **BrewSessionReferenceHandle; +typedef BrewSession **BrewSessionHandle; diff --git a/macbrew-ui/mbUtil.c b/macbrew-ui/mbUtil.c index 0194aaf..6d05041 100644 --- a/macbrew-ui/mbUtil.c +++ b/macbrew-ui/mbUtil.c @@ -1,3 +1,4 @@ +#include #include #include "mbUtil.h" #include "mbConstants.h" @@ -48,6 +49,12 @@ void CShowAlert(char *message) ShowAlert(pString); } +void PascalToCStringCopy(Str255 cStringOut, const Str255 pString) +{ + memcpy(cStringOut, (unsigned char *)pString, (char)pString[0] + 1); + p2cstr(cStringOut); +} + void MakeCursorBusy(void) { CursHandle x = GetCursor(watchCursor); diff --git a/macbrew-ui/mbUtil.h b/macbrew-ui/mbUtil.h index 62051fe..8c6c499 100644 --- a/macbrew-ui/mbUtil.h +++ b/macbrew-ui/mbUtil.h @@ -4,5 +4,6 @@ unsigned char GetCharFromBuffer(char *buffer, int offset); void Panic(Str255 message); void ShowAlert(Str255 message); void CShowAlert(char *message); +void PascalToCStringCopy(Str255 cStringOut, const Str255 pString); void MakeCursorBusy(void); void MakeCursorNormal(void); diff --git a/macbrew-ui/mbWViewSession.c b/macbrew-ui/mbWViewSession.c index 3fcfeb8..ef88137 100644 --- a/macbrew-ui/mbWViewSession.c +++ b/macbrew-ui/mbWViewSession.c @@ -4,7 +4,7 @@ typedef struct ViewSessionWindowState { - BrewSessionReferenceHandle brewSessionReferenceHandle; + BrewSessionHandle brewSessionHandle; } ViewSessionWindowState; static void ViewSessionWindowInitState(WindowPtr theWindow); @@ -54,20 +54,29 @@ void SessionViewWindowDestroy(WindowPtr window) } } -void SessionViewSetSession(WindowPtr window, BrewSessionReferenceHandle brewSessionReferenceHandle) +void SessionViewSetSession(WindowPtr window, BrewSessionHandle brewSessionHandle) { // Temporary function to test session select ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); - BrewSessionReference *brewSessionReference = NULL; - windowState->brewSessionReferenceHandle = brewSessionReferenceHandle; - HLock((Handle)brewSessionReferenceHandle); - brewSessionReference = *brewSessionReferenceHandle; - HLock((Handle)brewSessionReference->name); + BrewSession *brewSession = NULL; + windowState->brewSessionHandle = brewSessionHandle; + HLock((Handle)brewSessionHandle); + brewSession = *brewSessionHandle; + HLock((Handle)brewSession->recipe_title); + HLock((Handle)brewSession->phase); // Set title to demonstrate its working for now - SetWTitle(window, *(brewSessionReference->name)); + SetWTitle(window, *(brewSession->recipe_title)); - HUnlock((Handle)brewSessionReference->name); - HUnlock((Handle)brewSessionReferenceHandle); + SetPort(window); + TextFont(geneva); + TextSize(12); + + MoveTo(10, 10); + DrawString(*(brewSession->phase)); + + HUnlock((Handle)brewSession->phase); + HUnlock((Handle)brewSession->recipe_title); + HUnlock((Handle)brewSessionHandle); ViewSessionWindowUnlockState(window); } diff --git a/macbrew-ui/mbWViewSession.h b/macbrew-ui/mbWViewSession.h index 7c072f5..cbab018 100644 --- a/macbrew-ui/mbWViewSession.h +++ b/macbrew-ui/mbWViewSession.h @@ -1,6 +1,6 @@ -struct BrewSessionReference; +struct BrewSession; WindowPtr SessionViewWindowSetUp(void); void SessionViewWindowDestroy(WindowPtr window); -void SessionViewSetSession(WindowPtr window, struct BrewSessionReference **brewSessionReferenceHandle); +void SessionViewSetSession(WindowPtr window, struct BrewSession **brewSessionHandle); From 685bf06c8e62e195d419a7b0f650e597600cb699 Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Mon, 30 Aug 2021 10:11:10 +1000 Subject: [PATCH 06/10] feat: initial implementation of ferm data fetch - It now draws a graph but without labels --- .vscode/c_cpp_properties.json | 3 +- .vscode/settings.json | 6 +- macbrew-proxy/Cargo.toml | 3 +- .../resources/sample_fermentation.json | 5321 +++++++++++++++++ .../resources/sample_fermentation_manual.json | 50 + .../src/commands/get_fermentation.rs | 29 + macbrew-proxy/src/commands/mod.rs | 1 + .../brewers_friend/bf_api_data_manager.rs | 32 +- .../data/brewers_friend/bf_data_manager.rs | 5 +- .../src/data/brewers_friend/fermentation.rs | 25 + macbrew-proxy/src/data/brewers_friend/mod.rs | 1 + .../src/data/brewers_friend/sessions.rs | 25 + .../src/data/macbrew/fermentation.rs | 103 + macbrew-proxy/src/data/macbrew/mod.rs | 2 + macbrew-proxy/src/data/macbrew/sessions.rs | 8 +- macbrew-proxy/src/data/macbrew/shared.rs | 16 + macbrew-proxy/src/main.rs | 44 + ...acbrew_proxy__tests__get_fermentation.snap | 18 + ...proxy__tests__get_fermentation_manual.snap | 18 + .../macbrew_proxy__tests__get_sessions.snap | 7 +- macbrew-ui/MacBrewUI.rsrc.bin | Bin 5376 -> 5376 bytes macbrew-ui/mbConstants.h | 4 + macbrew-ui/mbDataManager.c | 131 +- macbrew-ui/mbDataManager.h | 4 +- macbrew-ui/mbMenus.c | 6 +- macbrew-ui/mbTypes.h | 18 + macbrew-ui/mbWViewSession.c | 133 +- macbrew-ui/mbWViewSession.h | 2 + 28 files changed, 5984 insertions(+), 31 deletions(-) create mode 100644 macbrew-proxy/resources/sample_fermentation.json create mode 100644 macbrew-proxy/resources/sample_fermentation_manual.json create mode 100644 macbrew-proxy/src/commands/get_fermentation.rs create mode 100644 macbrew-proxy/src/data/brewers_friend/fermentation.rs create mode 100644 macbrew-proxy/src/data/macbrew/fermentation.rs create mode 100644 macbrew-proxy/src/data/macbrew/shared.rs create mode 100644 macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation.snap create mode 100644 macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation_manual.snap diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index c11fe05..b23d22c 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -12,8 +12,7 @@ "${myDefaultIncludePath}" ], "defines": [ - "FOO", - "BAR=100" + "TARGET_OS_MAC" ], "forcedInclude": [ "${workspaceFolder}/include/MacIncludes.h" diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a92793..597c8b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,9 +34,11 @@ "cstdio": "c", "string.h": "c", "stdio.h": "c", - "textutils.h": "c" + "textutils.h": "c", + "quickdraw.h": "c", + "mbserial.h": "c" }, "c-cpp-flylint.clang.enable": false, "c-cpp-flylint.flexelint.enable": false, "c-cpp-flylint.lizard.enable": false -} \ No newline at end of file +} diff --git a/macbrew-proxy/Cargo.toml b/macbrew-proxy/Cargo.toml index 212c92d..545f14c 100644 --- a/macbrew-proxy/Cargo.toml +++ b/macbrew-proxy/Cargo.toml @@ -22,8 +22,9 @@ async-trait = "0.1.50" snafu = "0.6.10" quick-xml = { version = "0.22", features = [ "serialize" ] } hexplay = "0.2.1" +chrono = "0.4" [dev-dependencies] mockito = "0.30.0" insta = "1.7.1" -tokio-test = "0.4.2" \ No newline at end of file +tokio-test = "0.4.2" diff --git a/macbrew-proxy/resources/sample_fermentation.json b/macbrew-proxy/resources/sample_fermentation.json new file mode 100644 index 0000000..c4762df --- /dev/null +++ b/macbrew-proxy/resources/sample_fermentation.json @@ -0,0 +1,5321 @@ +{ + "message": "success", + "readings": [ + { + "id": "1102274", + "gravity": "1.042", + "gravity_unit": "G", + "temp": "", + "temp_unit": "C", + "ph": "", + "comment": "", + "eventtype": "Brew Day Complete", + "created_at": "2021-08-26T06:15:00+00:00", + "tank_id": null, + "source": "BrewLog", + "name": "", + "annotation": "0" + }, + { + "_id": { + "$oid": "6127343aebbf5f32ea101ae8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0420000000000011", + "gravity_unit": "G", + "temp": "69.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.68548212962", + "created_at": "2021-08-26T06:27:06+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612737bf5381293065126a76" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0420000000000011", + "gravity_unit": "G", + "temp": "69.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.69590467592", + "created_at": "2021-08-26T06:42:07+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61273b431e9e4766e8724661" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.041900000000001", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.70632376157", + "created_at": "2021-08-26T06:57:07+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61273ec7549a1762482927c4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0419666666666678", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.71674523148", + "created_at": "2021-08-26T07:12:07+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127424d3d2ad168c2641c43" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0418166666666673", + "gravity_unit": "G", + "temp": "73.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.72717793981", + "created_at": "2021-08-26T07:27:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612745d1e737c6324174d9c9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0417500000000006", + "gravity_unit": "G", + "temp": "73.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.73759927083", + "created_at": "2021-08-26T07:42:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127495a0f221547c721a04e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.041783333333334", + "gravity_unit": "G", + "temp": "73.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.748077743054", + "created_at": "2021-08-26T07:57:14+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61274cdf438b917d1c2bce74" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0418000000000007", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.75850050926", + "created_at": "2021-08-26T08:12:15+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61275063f532e169492191af" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0411833333333322", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.76892033565", + "created_at": "2021-08-26T08:27:15+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612753e9d2c6c5604e058e8a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0412666666666657", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.77935422453", + "created_at": "2021-08-26T08:42:17+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127576d85d238173e4b4f0d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0414999999999996", + "gravity_unit": "G", + "temp": "71.7", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.78977436343", + "created_at": "2021-08-26T08:57:17+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61275af1e737c6324174d9cc" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0412333333333323", + "gravity_unit": "G", + "temp": "71.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.80019584491", + "created_at": "2021-08-26T09:12:17+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61275e76d26b1a07857154b2" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0413333333333328", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.81061664352", + "created_at": "2021-08-26T09:27:18+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612761fa3e8759277a154daa" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0411499999999991", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.8210377662", + "created_at": "2021-08-26T09:42:18+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127657f89a53226345fd7c0" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0412833333333327", + "gravity_unit": "G", + "temp": "71.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.83147133102", + "created_at": "2021-08-26T09:57:19+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127690531e5fd52157c7346" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0412833333333327", + "gravity_unit": "G", + "temp": "73.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.84190331019", + "created_at": "2021-08-26T10:12:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61276c89b008b013d97efdcd" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0410999999999986", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.85232518518", + "created_at": "2021-08-26T10:27:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127700e3d52ca706102843e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.041033333333332", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.8627453125", + "created_at": "2021-08-26T10:42:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127739291beee490e56270c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.873167488426", + "created_at": "2021-08-26T10:57:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61277716fd88ab1b533124e7" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.88358765046", + "created_at": "2021-08-26T11:12:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61277a9cef42c3307245817a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.89401935185", + "created_at": "2021-08-26T11:27:24+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61277e20b9db3952b01f6804" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.904438715275", + "created_at": "2021-08-26T11:42:24+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612781a54ba4200f635b5292" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "72.1", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.914859606484", + "created_at": "2021-08-26T11:57:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127852a7562482ba81b7c77" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "72.3", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.925290891195", + "created_at": "2021-08-26T12:12:26+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612788aed2c6c5604e058e9d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.93571400463", + "created_at": "2021-08-26T12:27:26+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61278c32f4053156bd0cc8c3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.94613512731", + "created_at": "2021-08-26T12:42:26+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61278fb7dc18c66a2e7f39d5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.95655762731", + "created_at": "2021-08-26T12:57:27+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127933ce5a7b641ec6d22f5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.966979212964", + "created_at": "2021-08-26T13:12:28+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612796c107df26083e491add" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.97740181712", + "created_at": "2021-08-26T13:27:29+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61279a4538135e024c57d5ec" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.98782215278", + "created_at": "2021-08-26T13:42:29+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61279dc94d6b3754762ae206" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44434.99824365741", + "created_at": "2021-08-26T13:57:29+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127a14e6289965f681a186b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.008666655085", + "created_at": "2021-08-26T14:12:30+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127a4d3c860374b6e4dfce6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0409999999999986", + "gravity_unit": "G", + "temp": "71.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.01909913194", + "created_at": "2021-08-26T14:27:31+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127a858fcf9a47ad36a1256" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.040599999999999", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.029521111115", + "created_at": "2021-08-26T14:42:32+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127abdce99eaa7d3c1806ac" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.040016666666666", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.0399434838", + "created_at": "2021-08-26T14:57:32+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127af60c1c4914397530b36" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.03975", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.05036319444", + "created_at": "2021-08-26T15:12:32+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127b2e5d6876430db2f5a7d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0388333333333335", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.06078447916", + "created_at": "2021-08-26T15:27:33+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127b66bc69cbc0b31098727" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.037616666666666", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.071228900466", + "created_at": "2021-08-26T15:42:35+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127b9ef2e0e027b6d46a770" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0366666666666666", + "gravity_unit": "G", + "temp": "71.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.0816496875", + "created_at": "2021-08-26T15:57:35+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127bd73fc5fc97b966c23a5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.036000000000001", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.09206944444", + "created_at": "2021-08-26T16:12:35+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127c0f831f5be0f0202854d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.036000000000001", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.10249111111", + "created_at": "2021-08-26T16:27:36+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127c47d71ec1816ec41936b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.035066666666665", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.1129119676", + "created_at": "2021-08-26T16:42:37+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127c80197ca814a0410e48d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.035683333333334", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.12333331018", + "created_at": "2021-08-26T16:57:37+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127cb870b185e2e236b279f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0364666666666666", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.13376643518", + "created_at": "2021-08-26T17:12:39+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127cf0bb535583a883b4ed4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0371666666666655", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.144186701385", + "created_at": "2021-08-26T17:27:39+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127d28f37334b55cc11739f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0378999999999992", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.15460856481", + "created_at": "2021-08-26T17:42:39+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127d61312990d38541d1b73" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0383833333333328", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.165029212956", + "created_at": "2021-08-26T17:57:39+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127d998e9956f2a9d580255" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0381999999999993", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.175450937495", + "created_at": "2021-08-26T18:12:40+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127dd1dd6d71e6fd04f5a93" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0389333333333337", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.18588496527", + "created_at": "2021-08-26T18:27:41+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127e0a1d2c6c5604e058ed5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0389166666666667", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.19630534722", + "created_at": "2021-08-26T18:42:41+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127e426b535583a883b4ee9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.038833333333333", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.20672766203", + "created_at": "2021-08-26T18:57:42+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127e7ac2b7831120270cd53" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.038466666666666", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.217160231485", + "created_at": "2021-08-26T19:12:44+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127eb3018931c2bfc579e91" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0379833333333324", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.22758168981", + "created_at": "2021-08-26T19:27:44+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127eeb406685609e75fc84b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0376666666666656", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.238002465274", + "created_at": "2021-08-26T19:42:44+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127f239fe84413ed9041fd5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.036833333333333", + "gravity_unit": "G", + "temp": "71.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.248423506935", + "created_at": "2021-08-26T19:57:45+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127f5bf8157dc0c8c10b94b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0364666666666666", + "gravity_unit": "G", + "temp": "71.6", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.25886902777", + "created_at": "2021-08-26T20:12:47+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127f9444a1bfa181f4b2a6b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0360666666666667", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.269291168974", + "created_at": "2021-08-26T20:27:48+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6127fcc83b5f0477264b4959" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0357333333333332", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.27971189815", + "created_at": "2021-08-26T20:42:48+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128004cafb91b06c5564cfd" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0358833333333335", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.290133414346", + "created_at": "2021-08-26T20:57:48+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612803d17b5b84561200e9ce" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0360833333333335", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.3005655324", + "created_at": "2021-08-26T21:12:49+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61280758b67ad026f56041ba" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0360833333333335", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.31100971065", + "created_at": "2021-08-26T21:27:52+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61280adc9d170b08e1581e33" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.035833333333333", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.32142916666", + "created_at": "2021-08-26T21:42:52+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61280e62a4eb5832ed56ea82" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0352666666666663", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.33186298611", + "created_at": "2021-08-26T21:57:54+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612811e650da66725509df92" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0348999999999993", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.342284374994", + "created_at": "2021-08-26T22:12:54+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128156c3d52ca706102848d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0344999999999993", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.352717974536", + "created_at": "2021-08-26T22:27:56+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612818f018d67546734617df" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0336", + "gravity_unit": "G", + "temp": "71.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.36313925926", + "created_at": "2021-08-26T22:42:56+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61281c7450da66725509df9a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0335499999999995", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.373561238426", + "created_at": "2021-08-26T22:57:56+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61281ff9faf07d418e452275" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0335499999999997", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.383981782405", + "created_at": "2021-08-26T23:12:57+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128237d9c9efe7ea760e8a3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.034299999999999", + "gravity_unit": "G", + "temp": "71.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.39440194444", + "created_at": "2021-08-26T23:27:57+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61282701dfd7f67b866ba13c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0342166666666657", + "gravity_unit": "G", + "temp": "71.3", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.40482070602", + "created_at": "2021-08-26T23:42:57+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61282a867f7fbd22f52cf50e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0336999999999996", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.415241874995", + "created_at": "2021-08-26T23:57:58+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61282e0ac651ad1b12328b95" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0341333333333327", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.42566298611", + "created_at": "2021-08-27T00:12:58+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61283190d33a92450757fda2" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0333166666666667", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.43609689814", + "created_at": "2021-08-27T00:28:00+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612835149e215a0bf8042b09" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0319999999999998", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.44651754629", + "created_at": "2021-08-27T00:43:00+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61283899ca831668a8234d74" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0303166666666665", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.456938379626", + "created_at": "2021-08-27T00:58:01+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61283c1c5381293065126adc" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0302499999999997", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.46736135416", + "created_at": "2021-08-27T01:13:00+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61283fa2d9925d06f12a6270" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0302166666666663", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.477795555555", + "created_at": "2021-08-27T01:28:02+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612843269a20eb2f677534d7" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0298666666666665", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.48821719907", + "created_at": "2021-08-27T01:43:02+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612846ac2f1e154c81058cd3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.028733333333333", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.49865164352", + "created_at": "2021-08-27T01:58:04+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61284a30b231f37ba6543d96" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0291833333333333", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.50907118055", + "created_at": "2021-08-27T02:13:04+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61284db46705403fe83ea85f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0289333333333328", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.51949168981", + "created_at": "2021-08-27T02:28:04+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61285139a2c8e5745b3896b6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0285666666666662", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.529913032406", + "created_at": "2021-08-27T02:43:05+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612854bdde768a52c60885da" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0286500000000005", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.54033236111", + "created_at": "2021-08-27T02:58:05+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128584266ac8f07c8115cc9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0289499999999994", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.55075592592", + "created_at": "2021-08-27T03:13:06+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61285bc69c2edb656a310fd9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0286333333333328", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.561177592586", + "created_at": "2021-08-27T03:28:06+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61285f4badb52d072f7197e3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0280833333333328", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.571601400465", + "created_at": "2021-08-27T03:43:07+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612862d1e91f20675413945b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0275666666666667", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.58203515046", + "created_at": "2021-08-27T03:58:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61286655889337317b6df9f9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.027016666666667", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.592455115744", + "created_at": "2021-08-27T04:13:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612869d98b41fa34e2415748" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0264500000000005", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.602877210644", + "created_at": "2021-08-27T04:28:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61286d5d7b35ad3d9753afba" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.02645", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.6132984838", + "created_at": "2021-08-27T04:43:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612870e20179a13ad51bccbd" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0263333333333333", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.62372030093", + "created_at": "2021-08-27T04:58:10+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612874670cd79231b7035c74" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0253166666666667", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.634139814814", + "created_at": "2021-08-27T05:13:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612877eb6705403fe83ea86f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.025483333333334", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.64456212962", + "created_at": "2021-08-27T05:28:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61287b708f800b5184587bea" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.025783333333334", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.65498393519", + "created_at": "2021-08-27T05:43:12+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61287ef3cae05c09d22adedd" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0254000000000005", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.665405162035", + "created_at": "2021-08-27T05:58:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612882782946f9064b3658ea" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0248500000000005", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.67582657407", + "created_at": "2021-08-27T06:13:12+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612885ff98f43c1f3606e02d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0234500000000002", + "gravity_unit": "G", + "temp": "71.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.686282511575", + "created_at": "2021-08-27T06:28:15+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128898523a2c34aea42f26c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.023416666666667", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.69670381944", + "created_at": "2021-08-27T06:43:17+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61288d0a4d6b3754762ae265" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0225833333333336", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.70713771991", + "created_at": "2021-08-27T06:58:18+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128908d945e1f123f6b5eeb" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0226500000000003", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.7175584375", + "created_at": "2021-08-27T07:13:17+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61289412455c3b7b01622ee4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.022316666666667", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.72798164351", + "created_at": "2021-08-27T07:28:18+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612897971ebaa45cbe442761" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0213666666666668", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.73840127314", + "created_at": "2021-08-27T07:43:19+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61289b1dac4b3c34881b5784" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0213833333333335", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.748845381946", + "created_at": "2021-08-27T07:58:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61289ea265c5dd035d10fd30" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.020633333333334", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.75926645833", + "created_at": "2021-08-27T08:13:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128a2251ebaa45cbe442764" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0206000000000006", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.76968903935", + "created_at": "2021-08-27T08:28:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128a5ab18d6754673461800" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0201500000000001", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.78010832176", + "created_at": "2021-08-27T08:43:23+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128a9303d2ad168c2641ca3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0201000000000007", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.79054123842", + "created_at": "2021-08-27T08:58:24+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128acb576df1f51072f1b18" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.019366666666667", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.800973287034", + "created_at": "2021-08-27T09:13:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128b03a3a02061edf3fb1ec" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0192500000000004", + "gravity_unit": "G", + "temp": "72.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.811404733795", + "created_at": "2021-08-27T09:28:26+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128b3be0cd79231b7035c80" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0188333333333333", + "gravity_unit": "G", + "temp": "73.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.82182571759", + "created_at": "2021-08-27T09:43:26+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128b746e6abce0b0370062e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0191500000000002", + "gravity_unit": "G", + "temp": "73.7", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.832280324066", + "created_at": "2021-08-27T09:58:30+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128baca7b5b84561200ea03" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0183166666666665", + "gravity_unit": "G", + "temp": "74.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.84270123843", + "created_at": "2021-08-27T10:13:30+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128be4e65c5dd035d10fd3f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0180333333333331", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.853122581015", + "created_at": "2021-08-27T10:28:30+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128c1d37562482ba81b7cf9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0177333333333334", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.8635450463", + "created_at": "2021-08-27T10:43:31+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128c557d3b19b124256cf05" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0176833333333335", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.87396871528", + "created_at": "2021-08-27T10:58:31+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128c8ddbf6b9467eb501f74" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0177833333333335", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.884400682866", + "created_at": "2021-08-27T11:13:33+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128cc637cecc33a632d46b2" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0178999999999998", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.89483413194", + "created_at": "2021-08-27T11:28:35+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128cfe8549a176248292848" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0168500000000003", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.905268344904", + "created_at": "2021-08-27T11:43:36+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128d36dcc91dd27a75a64a0" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0164833333333332", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.91570173611", + "created_at": "2021-08-27T11:58:37+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128d6f4678a2053546e7244" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0154166666666673", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.9261480787", + "created_at": "2021-08-27T12:13:40+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128da7876288419ef19787a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0162166666666668", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.93656847222", + "created_at": "2021-08-27T12:28:40+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128ddffb538b978b227d080" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0145833333333334", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.94702356481", + "created_at": "2021-08-27T12:43:43+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128e185fcf9a47ad36a12c8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0144333333333333", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.95745679398", + "created_at": "2021-08-27T12:58:45+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128e5097f3f64563b1a2c65" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0172333333333332", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.967878460644", + "created_at": "2021-08-27T13:13:45+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128e88d6d460572934d4211" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0156500000000006", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.97829905093", + "created_at": "2021-08-27T13:28:45+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128ec14a4eb5832ed56eade" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0148000000000004", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.98873291666", + "created_at": "2021-08-27T13:43:48+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128ef97c651ad1b12328bc7" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0143000000000004", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44435.99915469907", + "created_at": "2021-08-27T13:58:47+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128f31c3cfff134c618f1ab" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0156833333333333", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.009575509255", + "created_at": "2021-08-27T14:13:48+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128f6a03e932c350f4538a6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0129833333333336", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.01999716435", + "created_at": "2021-08-27T14:28:48+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128fa257cecc33a632d46bb" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0130500000000005", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.03041912037", + "created_at": "2021-08-27T14:43:49+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6128fda92195ee558d584153" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0130500000000002", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.04084086806", + "created_at": "2021-08-27T14:58:49+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129012e81f08576755960b3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0117", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.05126226851", + "created_at": "2021-08-27T15:13:50+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612904b3d4936e121d435d8e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0116166666666666", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.061685671295", + "created_at": "2021-08-27T15:28:51+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129083c884a617c8c5a15be" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0115500000000002", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.07216403935", + "created_at": "2021-08-27T15:43:56+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61290bc10517d7605c4a085e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0104333333333337", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.0825976736", + "created_at": "2021-08-27T15:58:57+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61290f46e6abce0b03700658" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0109833333333333", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.093018449064", + "created_at": "2021-08-27T16:13:58+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612912ca23674b4c3763deeb" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.010383333333333", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.10343834491", + "created_at": "2021-08-27T16:28:58+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61291652aad21f032e34dd35" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0103333333333333", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.11390537037", + "created_at": "2021-08-27T16:44:02+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612919d7bcef0f5bae046604" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0106833333333334", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.12433818287", + "created_at": "2021-08-27T16:59:03+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61291d636f079a2d93343c80" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0113500000000004", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.13484068287", + "created_at": "2021-08-27T17:14:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612920e883e7141c042c7baa" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0103666666666673", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.14526320602", + "created_at": "2021-08-27T17:29:12+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129246c67e2ba095315de82" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0105666666666668", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.15568634259", + "created_at": "2021-08-27T17:44:12+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612927f05895a931902a8501" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0105666666666668", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.16610737268", + "created_at": "2021-08-27T17:59:12+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61292b75aad21f032e34dd47" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0098833333333337", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.17652989583", + "created_at": "2021-08-27T18:14:13+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61292efbe7119d55dd2d585e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0106000000000004", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.18697474537", + "created_at": "2021-08-27T18:29:15+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61293282c651ad1b12328be3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0106000000000006", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.19742071759", + "created_at": "2021-08-27T18:44:18+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612936070618715ab679a6cf" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0101499999999999", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.20785486111", + "created_at": "2021-08-27T18:59:19+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129398de7119d55dd2d5861" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0098999999999996", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.218286875", + "created_at": "2021-08-27T19:14:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61293d1167264707f840da61" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0096666666666665", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.2287078125", + "created_at": "2021-08-27T19:29:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129409681f08576755960dd" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0092166666666667", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.23912927083", + "created_at": "2021-08-27T19:44:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129441d24975c1586744fda" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.009633333333333", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.24958561343", + "created_at": "2021-08-27T19:59:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612947a4bcb2bb44e71f97ae" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0094999999999998", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.260031597216", + "created_at": "2021-08-27T20:14:28+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61294b2b1671ce21b027e960" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.009633333333333", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.27048756944", + "created_at": "2021-08-27T20:29:31+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61294eb4280aa061ec242c04" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0096999999999996", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.28096545139", + "created_at": "2021-08-27T20:44:36+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612952381ebaa45cbe4427a8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0095", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.291384976845", + "created_at": "2021-08-27T20:59:37+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612955c144d254464b35d7c3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.00935", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.301853206016", + "created_at": "2021-08-27T21:14:41+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129594b57c77e20bd3e8865" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0094166666666666", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.31234629629", + "created_at": "2021-08-27T21:29:47+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61295cd12475b60ba05e9c01" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0094", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.322778773145", + "created_at": "2021-08-27T21:44:49+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61296055d3b19b124256cf55" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0091000000000003", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.33320248842", + "created_at": "2021-08-27T21:59:49+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612963db5e130c20582be5ca" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.009016666666667", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.34363630787", + "created_at": "2021-08-27T22:14:51+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61296760a2c8e5745b389741" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0090000000000001", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.35405619213", + "created_at": "2021-08-27T22:29:52+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61296ae381f08576755960f8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0090000000000001", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.364477129624", + "created_at": "2021-08-27T22:44:51+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61296e695e130c20582be5cf" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0089500000000002", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.37491052083", + "created_at": "2021-08-27T22:59:53+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612971f2f23b4b77406448d7" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0089333333333337", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.3853884375", + "created_at": "2021-08-27T23:14:58+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612975771da319096b3f694b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0089666666666668", + "gravity_unit": "G", + "temp": "75.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.395810069436", + "created_at": "2021-08-27T23:29:59+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612978fb5e130c20582be5d3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0089166666666671", + "gravity_unit": "G", + "temp": "75.7", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.40623108796", + "created_at": "2021-08-27T23:44:59+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61297c7fb94e0c4b3e2a1543" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0089833333333336", + "gravity_unit": "G", + "temp": "76.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.416653402775", + "created_at": "2021-08-27T23:59:59+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612980041da319096b3f694d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008966666666667", + "gravity_unit": "G", + "temp": "76.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.427072650455", + "created_at": "2021-08-28T00:15:00+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612983887aec685f8968e5ea" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0089166666666671", + "gravity_unit": "G", + "temp": "77.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.437493900456", + "created_at": "2021-08-28T00:30:00+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129870c6b46b55ed64fbd72" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008866666666667", + "gravity_unit": "G", + "temp": "77.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.44791658565", + "created_at": "2021-08-28T00:45:00+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61298a93549a17624829289a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008716666666667", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.45834966435", + "created_at": "2021-08-28T01:00:03+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61298e17f7e97020971ee648" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0084666666666677", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.468771608794", + "created_at": "2021-08-28T01:15:03+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129919c0d33cc2069293c68" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0081666666666675", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.479206446755", + "created_at": "2021-08-28T01:30:04+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61299524889337317b6dfa5b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0081166666666674", + "gravity_unit": "G", + "temp": "78.5", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.48966072917", + "created_at": "2021-08-28T01:45:08+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612998a9f7e97020971ee64c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0080166666666677", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.50009459491", + "created_at": "2021-08-28T02:00:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61299c2dc955b16a8708c728" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0080166666666674", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.51051555556", + "created_at": "2021-08-28T02:15:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "61299fb144d254464b35d7e6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0084666666666675", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.52093508101", + "created_at": "2021-08-28T02:30:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129a336c4c9f46111249f3d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.531356423606", + "created_at": "2021-08-28T02:45:10+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129a6ba678a2053546e7298" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "78.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.54177635416", + "created_at": "2021-08-28T03:00:10+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129aa3fcb0bfa087e5687ab" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.55220907408", + "created_at": "2021-08-28T03:15:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129adc4cb2d74554d3edda5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.56263133101", + "created_at": "2021-08-28T03:30:12+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129b148533f7e31081f7ad8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.5730533912", + "created_at": "2021-08-28T03:45:12+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129b4cf3996185cb35b4159" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "78.1", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.58349857639", + "created_at": "2021-08-28T04:00:15+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129b854e40ccf54571d7d8f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.593920590276", + "created_at": "2021-08-28T04:15:16+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129bbd85c89e15c43356451" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "78.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.604340474536", + "created_at": "2021-08-28T04:30:16+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129bf5e34a7f72df8504509" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.614783923615", + "created_at": "2021-08-28T04:45:18+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129c2e372ff7e47821d9e72" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.625216458335", + "created_at": "2021-08-28T05:00:19+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129c6690b185e2e236b284e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.63564850694", + "created_at": "2021-08-28T05:15:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129c9ed31e5fd52157c744a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.64607163194", + "created_at": "2021-08-28T05:30:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129cd723d52ca7061028535" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "79.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.65649278935", + "created_at": "2021-08-28T05:45:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129d0f76705403fe83ea8e4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0078833333333344", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.66692515046", + "created_at": "2021-08-28T06:00:23+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129d47c3a65af1d992941c4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079500000000008", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.677347997684", + "created_at": "2021-08-28T06:15:24+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129d8006838e0678929e8e2" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079833333333343", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.68777106482", + "created_at": "2021-08-28T06:30:24+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129db85b7b47334e15fb2f1" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.008000000000001", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.698190613424", + "created_at": "2021-08-28T06:45:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129df09e8d73933903c2af8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079833333333341", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.708611574075", + "created_at": "2021-08-28T07:00:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129e28ed593bd580039a94a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079333333333342", + "gravity_unit": "G", + "temp": "78.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.719031678236", + "created_at": "2021-08-28T07:15:26+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129e61167264707f840daa6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007933333333334", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.72945100694", + "created_at": "2021-08-28T07:30:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129e9963a65af1d992941c9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079666666666676", + "gravity_unit": "G", + "temp": "79.1", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.73988320601", + "created_at": "2021-08-28T07:45:26+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129ed1b0618715ab679a6f6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079333333333342", + "gravity_unit": "G", + "temp": "79.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.7503067361", + "created_at": "2021-08-28T08:00:27+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129f09f57669b5fd1585bf3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0078000000000005", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.76072687499", + "created_at": "2021-08-28T08:15:27+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129f425e068540ebf55e532" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007516666666667", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.771158055555", + "created_at": "2021-08-28T08:30:29+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129f7a9e0c81176a9357ed9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079166666666675", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.781579641196", + "created_at": "2021-08-28T08:45:29+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129fb2e9c72b72dc074ee48" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0079166666666675", + "gravity_unit": "G", + "temp": "79.8", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.79200152777", + "created_at": "2021-08-28T09:00:30+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "6129feb2bd62c41db33f0072" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0078166666666675", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.80242398148", + "created_at": "2021-08-28T09:15:30+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a02376f787305e1734436" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007666666666667", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.812845208326", + "created_at": "2021-08-28T09:30:31+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a05bf87b01829ad1eabb8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0075333333333334", + "gravity_unit": "G", + "temp": "78.7", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.823323819444", + "created_at": "2021-08-28T09:45:35+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a0945f7e97020971ee68d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0075500000000004", + "gravity_unit": "G", + "temp": "79.8", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.83374652778", + "created_at": "2021-08-28T10:00:37+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a0ccaaf094224634d46ee" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0075666666666667", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.84417940972", + "created_at": "2021-08-28T10:15:38+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a104e87b01829ad1eabbf" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.00755", + "gravity_unit": "G", + "temp": "78.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.85460046296", + "created_at": "2021-08-28T10:30:38+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a13d2cb4e290936384458" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007383333333333", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.86502234953", + "created_at": "2021-08-28T10:45:38+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a1757e40ccf54571d7da6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0076333333333334", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.87544353009", + "created_at": "2021-08-28T11:00:39+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a1adb7b35ad3d9753b026" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0074833333333333", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.88586606482", + "created_at": "2021-08-28T11:15:39+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a1e6044d254464b35d815" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0074166666666666", + "gravity_unit": "G", + "temp": "78.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.896287986114", + "created_at": "2021-08-28T11:30:40+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a21e4c955b16a8708c75b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0074333333333334", + "gravity_unit": "G", + "temp": "79.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.906707951384", + "created_at": "2021-08-28T11:45:40+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a25699a20eb2f6775357f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0074166666666666", + "gravity_unit": "G", + "temp": "79.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.91713026621", + "created_at": "2021-08-28T12:00:41+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a28ed0aab7309fa14e9b6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0072833333333333", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.92755025463", + "created_at": "2021-08-28T12:15:41+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a2c73e120e350944f9025" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0071666666666663", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.937985624994", + "created_at": "2021-08-28T12:30:43+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a2ff75659635a763adb12" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0072499999999995", + "gravity_unit": "G", + "temp": "79.1", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.948405393516", + "created_at": "2021-08-28T12:45:43+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a337b0aab7309fa14e9b8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0071999999999997", + "gravity_unit": "G", + "temp": "79.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.95882444444", + "created_at": "2021-08-28T13:00:43+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a3701194cdf247b174406" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0071333333333328", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.96925699074", + "created_at": "2021-08-28T13:15:45+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a3a85678a2053546e72d8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0070666666666659", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.979677523144", + "created_at": "2021-08-28T13:30:45+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a3e0a8c72054ba32b1fd4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0071666666666663", + "gravity_unit": "G", + "temp": "79.1", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44436.99009782407", + "created_at": "2021-08-28T13:45:46+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a418d9828a64bc712f793" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0072666666666665", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.00052023148", + "created_at": "2021-08-28T14:00:45+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a451208f5033f76524e43" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0070999999999994", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.01094137731", + "created_at": "2021-08-28T14:15:46+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a489887b01829ad1eabd4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0070833333333329", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.021383831016", + "created_at": "2021-08-28T14:30:48+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a4c1e5e28d15b371de40c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.031817592586", + "created_at": "2021-08-28T14:45:50+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a4fa4894f830aad25757c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007016666666666", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.04225965277", + "created_at": "2021-08-28T15:00:52+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a5329b42e260f5a30dde7" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007016666666666", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.05268137731", + "created_at": "2021-08-28T15:15:53+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a56aecad2281e15243d2c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0070333333333328", + "gravity_unit": "G", + "temp": "79.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.06311618056", + "created_at": "2021-08-28T15:30:54+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a5a329fe47d09fa2b1f88" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.07353695602", + "created_at": "2021-08-28T15:45:54+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a5db6f2e3185509489e02" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.083958379626", + "created_at": "2021-08-28T16:00:54+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a613bcb0bfa087e5687e2" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007016666666666", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.09438034722", + "created_at": "2021-08-28T16:15:55+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a64bf9fe47d09fa2b1f92" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007016666666666", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.10479900463", + "created_at": "2021-08-28T16:30:55+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a68435659635a763adb2f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.5", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.11522005787", + "created_at": "2021-08-28T16:45:55+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a6bc8d3b19b124256cfa4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.007016666666666", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.12564239583", + "created_at": "2021-08-28T17:00:56+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a6f4de068540ebf55e556" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.136063148144", + "created_at": "2021-08-28T17:15:57+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a72d19bdce31ff57a7bc8" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.14648412036", + "created_at": "2021-08-28T17:30:57+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a76556705403fe83ea925" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.15690663194", + "created_at": "2021-08-28T17:45:57+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a79da80d3aa12d67526ec" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.16732825231", + "created_at": "2021-08-28T18:00:58+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a7d5e0a2a7902ef47aab5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.17774951389", + "created_at": "2021-08-28T18:15:58+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a80e5d568027bab2ea083" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.18819491898", + "created_at": "2021-08-28T18:31:01+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a84693a65af1d99294216" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.19861554397", + "created_at": "2021-08-28T18:46:01+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a87ede802bc1ae1326c10" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.209037280096", + "created_at": "2021-08-28T19:01:01+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a8b72e99eaa7d3c1807ba" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.21945703704", + "created_at": "2021-08-28T19:16:02+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a8ef6287235470866d926" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.22987875", + "created_at": "2021-08-28T19:31:02+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a927dcf58c6565940911d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.24032239583", + "created_at": "2021-08-28T19:46:05+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a96015c89e15c43356493" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.3", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.25074247685", + "created_at": "2021-08-28T20:01:05+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a9986cad2281e15243d54" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.26116386574", + "created_at": "2021-08-28T20:16:06+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612a9d0a148d032650779de6" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.27158545139", + "created_at": "2021-08-28T20:31:06+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aa08e1fdd856ac939ff5f" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.282005381945", + "created_at": "2021-08-28T20:46:06+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aa41221daab1c704ff72e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.29242586805", + "created_at": "2021-08-28T21:01:06+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aa797f59d8031eb04179b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.5", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.302846932864", + "created_at": "2021-08-28T21:16:07+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aab1bcce46578a435b489" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.31326768519", + "created_at": "2021-08-28T21:31:07+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aae9f4d897f65c469d393" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.5", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.32368780093", + "created_at": "2021-08-28T21:46:07+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ab2243d58b16f4a3d8af2" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.9", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.33410943287", + "created_at": "2021-08-28T22:01:08+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ab5a81cba3b7b1c3125d5" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.34452968749", + "created_at": "2021-08-28T22:16:08+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ab92d10498a4d0b513b9e" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.35495047454", + "created_at": "2021-08-28T22:31:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612abcb1e1b73d27cd516e86" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.36538274305", + "created_at": "2021-08-28T22:46:09+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ac0365f789a1d9125313b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.37580523147", + "created_at": "2021-08-28T23:01:10+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ac3bbbd62c41db33f00b0" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.8", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.386228055555", + "created_at": "2021-08-28T23:16:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ac73f569dd226d030a278" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.396649201386", + "created_at": "2021-08-28T23:31:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612acac35f789a1d91253143" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.407070775465", + "created_at": "2021-08-28T23:46:11+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ace49bd62c41db33f00b2" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.3", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.417504895835", + "created_at": "2021-08-29T00:01:13+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ad1cd25fe6809ab250540" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.42792568287", + "created_at": "2021-08-29T00:16:13+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ad552c50ae955461fd3d3" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.4", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.438347164345", + "created_at": "2021-08-29T00:31:14+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ad8d8b7b47334e15fb349" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.44878119213", + "created_at": "2021-08-29T00:46:16+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612adc5e6642f41f5f2c554a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "80.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.45922625", + "created_at": "2021-08-29T01:01:18+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612adfe3678a2053546e731d" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069666666666661", + "gravity_unit": "G", + "temp": "79.2", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.46964679398", + "created_at": "2021-08-29T01:16:19+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ae3679828a64bc712f7d7" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069666666666661", + "gravity_unit": "G", + "temp": "79.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.480080717585", + "created_at": "2021-08-29T01:31:19+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612ae6ec5349396ca8133a78" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069833333333327", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.4905015162", + "created_at": "2021-08-29T01:46:20+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aea719db48d6c982c8a75" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "78.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.50093311343", + "created_at": "2021-08-29T02:01:21+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aedf687b01829ad1eac12" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069999999999992", + "gravity_unit": "G", + "temp": "77.5", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.51135474537", + "created_at": "2021-08-29T02:16:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612af17a8cbb2d14ba57cc6a" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069666666666661", + "gravity_unit": "G", + "temp": "77.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.521777326394", + "created_at": "2021-08-29T02:31:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612af4fe0d33cc2069293cf9" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069666666666661", + "gravity_unit": "G", + "temp": "77.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.532198912035", + "created_at": "2021-08-29T02:46:22+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612af88310498a4d0b513bbc" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069666666666661", + "gravity_unit": "G", + "temp": "76.8", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.5426192824", + "created_at": "2021-08-29T03:01:23+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612afc088cbb2d14ba57cc6b" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069499999999993", + "gravity_unit": "G", + "temp": "76.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.553052337964", + "created_at": "2021-08-29T03:16:24+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612aff8d7d0473456017f72c" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069166666666662", + "gravity_unit": "G", + "temp": "76.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.56347371528", + "created_at": "2021-08-29T03:31:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612b031142210a300d61dff1" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0068666666666661", + "gravity_unit": "G", + "temp": "76.0", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.573894733796", + "created_at": "2021-08-29T03:46:25+00:00", + "source": "tilt" + }, + { + "_id": { + "$oid": "612b0695d560fa611b2bdcb4" + }, + "login_id": 217955, + "brewevent_id": 400923, + "recipe_id": 1188403, + "ip": "159.196.171.56", + "name": "BLUE", + "gravity": "1.0069499999999993", + "gravity_unit": "G", + "temp": "75.1", + "temp_unit": "F", + "comment": "", + "beer": "Fermenter 1", + "interval": "44437.58431697917", + "created_at": "2021-08-29T04:01:25+00:00", + "source": "tilt" + } + ] +} diff --git a/macbrew-proxy/resources/sample_fermentation_manual.json b/macbrew-proxy/resources/sample_fermentation_manual.json new file mode 100644 index 0000000..c2d4549 --- /dev/null +++ b/macbrew-proxy/resources/sample_fermentation_manual.json @@ -0,0 +1,50 @@ +{ + "message": "success", + "readings": [ + { + "id": "959187", + "gravity": "1.055", + "gravity_unit": "G", + "temp": "21", + "temp_unit": "C", + "ph": "", + "comment": "", + "eventtype": "Brew Day Complete", + "created_at": "2020-08-06T11:39:00+00:00", + "tank_id": null, + "source": "BrewLog", + "name": "", + "annotation": "0" + }, + { + "id": "961491", + "gravity": "1.01", + "gravity_unit": "G", + "temp": "20.5", + "temp_unit": "C", + "ph": "", + "comment": "", + "eventtype": "Sample", + "created_at": "2020-08-12T01:55:13+00:00", + "tank_id": null, + "source": "BrewLog", + "name": "", + "annotation": "0" + }, + { + "id": "962433", + "gravity": "1.01", + "gravity_unit": "G", + "temp": "20", + "temp_unit": "C", + "ph": "", + "comment": "", + "eventtype": "Fermentation Complete", + "created_at": "2020-08-13T06:02:00+00:00", + "tank_id": null, + "source": "BrewLog", + "name": "", + "annotation": "0" + } + ] +} diff --git a/macbrew-proxy/src/commands/get_fermentation.rs b/macbrew-proxy/src/commands/get_fermentation.rs new file mode 100644 index 0000000..ef7fc53 --- /dev/null +++ b/macbrew-proxy/src/commands/get_fermentation.rs @@ -0,0 +1,29 @@ +use crate::commands::command::{prepare_response, Command}; +use crate::data::brewers_friend::bf_data_manager::BfDataManager; +use crate::data::macbrew::fermentation::FermentationData; +use crate::error::Error::InvalidCommandInput; +use crate::error::Result; +use async_trait::async_trait; +use std::marker::PhantomData; + +// TODO: Work out why rustc thinks T is unused. Is PhantomData needed? Can I restructure this? +// Clue: https://github.com/rust-lang/rust/issues/23246 +pub struct GetFermentationDataCommand(PhantomData); + +#[async_trait] +impl Command + for GetFermentationDataCommand +{ + async fn handle(rid: &str, args: &[&str]) -> Result> { + match args { + [session_id] => { + let bf_fermentation = DataManager::fermentation(session_id).await?; + let fermentation_data = FermentationData::from_bf_fermentation(&bf_fermentation); + prepare_response(rid, true, &fermentation_data) + } + [_args @ ..] => Err(InvalidCommandInput { + message: String::from("Expected single parameter (Session ID)"), + }), + } + } +} diff --git a/macbrew-proxy/src/commands/mod.rs b/macbrew-proxy/src/commands/mod.rs index 952501a..655126f 100644 --- a/macbrew-proxy/src/commands/mod.rs +++ b/macbrew-proxy/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod command; +pub mod get_fermentation; pub mod get_recipes; pub mod get_sessions; pub mod list_sessions; diff --git a/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs b/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs index cbc5bc6..a0c0110 100644 --- a/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs +++ b/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs @@ -1,6 +1,9 @@ use crate::data::brewers_friend::bf_data_manager::BfDataManager; +use crate::data::brewers_friend::fermentation::BfFermentationResponse; use crate::data::brewers_friend::recipes::BeerXml; -use crate::data::brewers_friend::sessions::{BfBrewSession, BfBrewSessionsResponse}; +use crate::data::brewers_friend::sessions::{ + BfBrewSession, BfBrewSessionFull, BfBrewSessionsFullResponse, BfBrewSessionsResponse, +}; use crate::error::{Error, Result}; use async_trait::async_trait; use reqwest::header::HeaderMap; @@ -59,7 +62,7 @@ impl BfDataManager for BfApiDataManager { Ok(response.brewsessions) } - async fn session(id: &str) -> Result { + async fn session(id: &str) -> Result { println!("Fetching session with id [{}]...", id); let client = create_client()?; @@ -69,7 +72,7 @@ impl BfDataManager for BfApiDataManager { let body = res.text().await?; - let response: BfBrewSessionsResponse = serde_json::from_str(&body)?; + let response: BfBrewSessionsFullResponse = serde_json::from_str(&body)?; let session = match response.brewsessions.as_slice() { [] => Err(Error::ApiResponseValidationError { @@ -85,6 +88,29 @@ impl BfDataManager for BfApiDataManager { session } + async fn fermentation(session_id: &str) -> Result { + println!( + "Fetching fermentation data for session with id [{}]...", + session_id + ); + let client = create_client()?; + + let brew_session_url = format!("{}/v1/fermentation/{}", get_base_url(), session_id); + + let res = client.get(&brew_session_url).send().await?; + + let body = res.text().await?; + + let response: BfFermentationResponse = serde_json::from_str(&body)?; + + println!( + "Fetched fermentation data for session with id [{}]...", + session_id + ); + + Ok(response) + } + async fn recipe(recipe_id: &str) -> Result { println!("Fetching recipe with id [{}]...", recipe_id); let client = create_client()?; diff --git a/macbrew-proxy/src/data/brewers_friend/bf_data_manager.rs b/macbrew-proxy/src/data/brewers_friend/bf_data_manager.rs index 5453cc7..bf7407a 100644 --- a/macbrew-proxy/src/data/brewers_friend/bf_data_manager.rs +++ b/macbrew-proxy/src/data/brewers_friend/bf_data_manager.rs @@ -1,11 +1,14 @@ +use crate::data::brewers_friend::fermentation::BfFermentationResponse; use crate::data::brewers_friend::recipes::BeerXml; use crate::data::brewers_friend::sessions::BfBrewSession; +use crate::data::brewers_friend::sessions::BfBrewSessionFull; use crate::Result; use async_trait::async_trait; #[async_trait] pub trait BfDataManager { async fn sessions() -> Result>; - async fn session(session_id: &str) -> Result; + async fn session(session_id: &str) -> Result; + async fn fermentation(session_id: &str) -> Result; async fn recipe(recipe_id: &str) -> Result; } diff --git a/macbrew-proxy/src/data/brewers_friend/fermentation.rs b/macbrew-proxy/src/data/brewers_friend/fermentation.rs new file mode 100644 index 0000000..ac685a0 --- /dev/null +++ b/macbrew-proxy/src/data/brewers_friend/fermentation.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct BfFermentationReading { + pub login_id: Option, + pub brewevent_id: Option, + pub recipe_id: Option, + pub ip: Option, + pub name: String, + pub gravity: String, + pub gravity_unit: String, + pub temp: String, + pub temp_unit: String, + pub comment: String, + pub beer: Option, + pub interval: Option, + pub created_at: String, + pub source: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BfFermentationResponse { + pub message: String, + pub readings: Vec, +} diff --git a/macbrew-proxy/src/data/brewers_friend/mod.rs b/macbrew-proxy/src/data/brewers_friend/mod.rs index 8625f83..278c8ce 100644 --- a/macbrew-proxy/src/data/brewers_friend/mod.rs +++ b/macbrew-proxy/src/data/brewers_friend/mod.rs @@ -1,4 +1,5 @@ pub mod bf_api_data_manager; pub mod bf_data_manager; +pub mod fermentation; pub mod recipes; pub mod sessions; diff --git a/macbrew-proxy/src/data/brewers_friend/sessions.rs b/macbrew-proxy/src/data/brewers_friend/sessions.rs index f291c7e..1edad37 100644 --- a/macbrew-proxy/src/data/brewers_friend/sessions.rs +++ b/macbrew-proxy/src/data/brewers_friend/sessions.rs @@ -1,5 +1,12 @@ use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BfBrewSessionRecipe { + pub id: String, + pub stylename: String, + pub created_at: String, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BfBrewSession { pub id: String, @@ -9,9 +16,27 @@ pub struct BfBrewSession { pub recipeid: String, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BfBrewSessionFull { + pub id: String, + pub phase: String, + pub batchcode: String, + pub recipe_title: String, + pub recipeid: String, + pub recipe: BfBrewSessionRecipe, + pub created_at: String, +} + #[derive(Serialize, Deserialize, Debug)] pub struct BfBrewSessionsResponse { pub message: String, pub count: String, pub brewsessions: Vec, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct BfBrewSessionsFullResponse { + pub message: String, + pub count: String, + pub brewsessions: Vec, +} diff --git a/macbrew-proxy/src/data/macbrew/fermentation.rs b/macbrew-proxy/src/data/macbrew/fermentation.rs new file mode 100644 index 0000000..0983a11 --- /dev/null +++ b/macbrew-proxy/src/data/macbrew/fermentation.rs @@ -0,0 +1,103 @@ +use crate::data::brewers_friend::fermentation::{BfFermentationReading, BfFermentationResponse}; +use crate::data::macbrew::shared::{iso_string_to_mac_epoch, MacEpoch}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +type GraphPoint = (i8, u8); + +/** + * Ensures that the temp reading is in Celsius because I can't read Fahrenheit + * TODO: Make this switchable somewhere for US users + */ +fn extract_temp_from_reading(reading: &BfFermentationReading) -> f64 { + // TODO: Handle parse error + let raw_temp = f64::from_str(&reading.temp).unwrap(); + match reading.temp_unit.as_str() { + "F" => (raw_temp - 32f64) * 0.5556f64, + "C" => raw_temp, + // TODO: Handle invalid value + &_ => raw_temp, + } +} + +#[repr(C)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FermentationData { + pub graph: Vec, + pub start_date: MacEpoch, + pub end_date: MacEpoch, +} + +impl FermentationData { + pub fn from_bf_fermentation( + fermentation_response: &BfFermentationResponse, + ) -> FermentationData { + // Width of the graph control in pixels + let width = 320; + // How many pixels per point (saves bandwidth) + let divider = 4; + // How many buckets (data points) needed for the graph + let bucket_count = width / divider; + // Filter out any readings that are missing temp or gravity + // TODO: Manual entries can have one or the other, maybe we need to + // do something like fill in missing values with previous values. + // This is fine for my current use case though (getting readings from a Tilt) + let filtered_readings: Vec<_> = fermentation_response + .readings + .iter() + .filter(|reading| !reading.temp.is_empty() && !reading.gravity.is_empty()) + .collect(); + let reading_count = filtered_readings.len(); + + // Allocate each data point + let mut buckets = vec![(0, 0); bucket_count]; + + // Fill in the pre allocated vectore with points + // TODO: This is a bad way to do it and assumes that the data points are spread evenly + // what we really need to do is use time as the index and do a linear interpolation + // to fill in missing points. It will do to test the Mac UI control + for (bucket_index, point) in buckets.iter_mut().enumerate() { + // Group each reading into a "bucket" that covers the size of a data point + let readings_per_bucket = reading_count / bucket_count; + let reading_index = bucket_index * readings_per_bucket; + let readings_to_average = + &filtered_readings[reading_index..reading_index + readings_per_bucket]; + + // Average the readings over an entire bucket + let average_temp_for_bucket: f64 = readings_to_average + .iter() + .map(|reading| extract_temp_from_reading(reading)) + .sum::() + / (readings_per_bucket as f64); + + let average_gravity_for_bucket: f64 = readings_to_average + .iter() + // TODO: Handle parse error + .map(|reading| f64::from_str(&reading.gravity).unwrap()) + .sum::() + / (readings_per_bucket as f64); + + // Storing the literal SG reading as fixed point or string is wasteful + // If we ignore that gravity can dip slightly below 1.000 sometimes and assume + // that gravity will never be higher that 1.256 (a safe assumption), we can drop + // the 1 and convert it for display purposes on the Mac side + // TODO: Extract to functions + let fixed_point_gravity = ((average_gravity_for_bucket * 1000f64).round() as i32 + - 1000) + .clamp(0, u8::MAX.into()); + + *point = (average_temp_for_bucket as i8, fixed_point_gravity as u8); + } + + FermentationData { + graph: buckets, + // TODO: Handle 0 size readings error properly + start_date: iso_string_to_mac_epoch( + &fermentation_response.readings.first().unwrap().created_at, + ), + end_date: iso_string_to_mac_epoch( + &fermentation_response.readings.last().unwrap().created_at, + ), + } + } +} diff --git a/macbrew-proxy/src/data/macbrew/mod.rs b/macbrew-proxy/src/data/macbrew/mod.rs index f8369b2..ccf8d4b 100644 --- a/macbrew-proxy/src/data/macbrew/mod.rs +++ b/macbrew-proxy/src/data/macbrew/mod.rs @@ -1,2 +1,4 @@ +pub mod fermentation; pub mod recipes; pub mod sessions; +pub mod shared; diff --git a/macbrew-proxy/src/data/macbrew/sessions.rs b/macbrew-proxy/src/data/macbrew/sessions.rs index 42ee42a..60970eb 100644 --- a/macbrew-proxy/src/data/macbrew/sessions.rs +++ b/macbrew-proxy/src/data/macbrew/sessions.rs @@ -1,4 +1,5 @@ use crate::data::brewers_friend::sessions::BfBrewSession; +use crate::data::brewers_friend::sessions::BfBrewSessionFull; use serde::{Deserialize, Serialize}; /// Enough information to decide which session to fetch more info on @@ -10,6 +11,7 @@ pub struct BrewSessionReference { name: String, } +#[repr(C)] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BrewSession { pub id: String, @@ -17,6 +19,8 @@ pub struct BrewSession { pub batch_code: String, pub recipe_title: String, pub recipe_id: String, + pub style_name: String, + pub created_at: String, } impl BrewSessionReference { @@ -30,13 +34,15 @@ impl BrewSessionReference { } impl BrewSession { - pub fn from_bf_session(session: &BfBrewSession) -> BrewSession { + pub fn from_bf_session(session: &BfBrewSessionFull) -> BrewSession { BrewSession { id: session.id.clone(), phase: session.phase.clone(), batch_code: session.batchcode.clone(), recipe_title: session.recipe_title.clone(), recipe_id: session.recipeid.clone(), + style_name: session.recipe.stylename.clone(), + created_at: session.created_at.clone(), } } } diff --git a/macbrew-proxy/src/data/macbrew/shared.rs b/macbrew-proxy/src/data/macbrew/shared.rs new file mode 100644 index 0000000..89e6c69 --- /dev/null +++ b/macbrew-proxy/src/data/macbrew/shared.rs @@ -0,0 +1,16 @@ +use chrono::DateTime; + +/** + * This date-time information is expressed, using 4 bytes, as the number of seconds elapsed since midnight, January 1, 1904. + */ +pub type MacEpoch = u32; + +pub fn iso_string_to_mac_epoch(iso_string: &str) -> MacEpoch { + // TODO: Handle parse error + let parsed_date = DateTime::parse_from_rfc3339(iso_string).unwrap(); + let unix_epoch = DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap(); + let mac_epoch = DateTime::parse_from_rfc3339("1904-01-01T00:00:00Z").unwrap(); + let epoch_difference = mac_epoch.signed_duration_since(unix_epoch); + // Returns the number of non-leap seconds since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp"). + (parsed_date.timestamp() + epoch_difference.num_seconds()) as MacEpoch +} diff --git a/macbrew-proxy/src/main.rs b/macbrew-proxy/src/main.rs index 5ab8822..fb72c86 100644 --- a/macbrew-proxy/src/main.rs +++ b/macbrew-proxy/src/main.rs @@ -51,6 +51,12 @@ async fn handle_line(line: &str) -> Result> { ) .await } + ["GET", "FERMENTATION", args @ ..] => { + commands::get_fermentation::GetFermentationDataCommand::::handle( + request_id, args, + ) + .await + } ["GET", "RECIPE", args @ ..] => { commands::get_recipes::GetRecipesCommand::::handle( request_id, args, @@ -221,6 +227,44 @@ mod tests { insta::assert_debug_snapshot!("get_sessions", format_binary_for_snap(&result)); } + #[tokio::test] + async fn test_get_fermentation() { + let session_json = include_str!("../resources/sample_fermentation.json"); + let _m = mock("GET", "/v1/fermentation/363597") + .with_status(200) + .with_header("content-type", "application/xml") + .with_header("x-api-key", "1234") + .with_body(session_json) + .create(); + + let result = handle_line("66 GET FERMENTATION 363597").await.unwrap(); + + assert_length(&result); + assert_checksum(&result); + assert_request_id(&result, "66"); + insta::assert_debug_snapshot!("get_fermentation", format_binary_for_snap(&result)); + } + + #[tokio::test] + async fn test_get_fermentation_manual() { + // Readings were done manually without a device like a Tilt + // So there aren't many points + let session_json = include_str!("../resources/sample_fermentation_manual.json"); + let _m = mock("GET", "/v1/fermentation/350900") + .with_status(200) + .with_header("content-type", "application/xml") + .with_header("x-api-key", "1234") + .with_body(session_json) + .create(); + + let result = handle_line("11 GET FERMENTATION 350900").await.unwrap(); + + assert_length(&result); + assert_checksum(&result); + assert_request_id(&result, "11"); + insta::assert_debug_snapshot!("get_fermentation_manual", format_binary_for_snap(&result)); + } + #[tokio::test] async fn test_ping() { let result = handle_line("1 PING").await.unwrap(); diff --git a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation.snap b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation.snap new file mode 100644 index 0000000..7cbe573 --- /dev/null +++ b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation.snap @@ -0,0 +1,18 @@ +--- +source: macbrew-proxy/src/main.rs +expression: format_binary_for_snap(&result) +--- +[ + "00000000 00 AF 00 02 36 36 01 00 50 15 2A 16 2A 16 2A 15 | .».☻66☺.P§*▬*▬*§ |", + "00000010 29 15 29 16 29 15 29 16 29 15 29 15 29 16 29 15 | )§)▬)§)▬)§)§)▬)§ |", + "00000020 28 15 26 15 24 15 24 15 26 15 27 15 26 15 24 15 | (§&§$§$§&§'§&§$§ |", + "00000030 24 15 24 15 22 16 22 15 22 15 20 15 1E 15 1D 15 | $§$§\"▬\"§\"§ §▲§↔§ |", + "00000040 1D 15 1C 15 1B 15 1A 15 19 16 17 16 16 16 15 16 | ↔§∟§←§→§↓▬↨▬▬▬§▬ |", + "00000050 14 16 13 17 12 17 12 17 10 17 0F 17 10 17 0E 17 | ¶▬‼↨↕↨↕↨►↨☼↨►↨♫↨ |", + "00000060 0D 17 0B 17 0B 17 0B 17 0A 17 0A 17 0A 17 0A 17 | ♪↨♂↨♂↨♂↨◙↨◙↨◙↨◙↨ |", + "00000070 0A 17 09 17 09 17 09 18 09 19 09 19 08 19 08 19 | ◙↨○↨○↨○↑○↓○↓◘↓◘↓ |", + "00000080 08 19 08 19 08 19 08 19 08 1A 08 19 08 19 08 1A | ◘↓◘↓◘↓◘↓◘→◘↓◘↓◘→ |", + "00000090 08 1A 08 19 08 19 08 1A 07 1A 07 19 07 1A 07 1A | ◘→◘↓◘↓◘→•→•↓•→•→ |", + "000000A0 07 19 07 1A 07 19 07 1A 07 E5 01 80 E4 E5 05 56 | •↓•→•↓•→•σ☺Ç∑σ♣V |", + "000000B0 15 3A DB 87 34 | §:█ç4 |", +] diff --git a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation_manual.snap b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation_manual.snap new file mode 100644 index 0000000..1f373d8 --- /dev/null +++ b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_fermentation_manual.snap @@ -0,0 +1,18 @@ +--- +source: macbrew-proxy/src/main.rs +expression: format_binary_for_snap(&result) +--- +[ + "00000000 00 AF 00 02 31 31 01 00 50 00 00 00 00 00 00 00 | .».☻11☺.P....... |", + "00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ |", + "000000A0 00 00 00 00 00 00 00 00 00 E3 06 3B 54 E3 0F 26 | .........∏♠;T∏☼& |", + "000000B0 D8 08 1F ED 31 | ╪◘▼Ø1 |", +] diff --git a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap index da042de..3f2c7fc 100644 --- a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap +++ b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap @@ -3,8 +3,11 @@ source: macbrew-proxy/src/main.rs expression: format_binary_for_snap(&result) --- [ - "00000000 00 37 00 02 33 33 01 00 06 33 36 33 35 39 37 00 | .7.☻33☺.♠363597. |", + "00000000 00 5F 00 02 33 33 01 00 06 33 36 33 35 39 37 00 | ._.☻33☺.♠363597. |", "00000010 08 41 6C 6C 20 47 6F 6E 65 00 05 46 46 30 30 32 | ◘All Gone.♣FF002 |", "00000020 00 0E 46 6F 72 72 65 73 74 79 20 46 72 75 69 74 | .♫Forresty Fruit |", - "00000030 00 07 31 30 37 32 39 36 31 54 6F 79 4A | .•1072961ToyJ |", + "00000030 00 07 31 30 37 32 39 36 31 00 11 41 6D 65 72 69 | .•1072961.◄Ameri |", + "00000040 63 61 6E 20 50 61 6C 65 20 41 6C 65 00 13 32 30 | can Pale Ale.‼20 |", + "00000050 32 30 2D 31 31 2D 30 33 20 32 31 3A 30 35 3A 34 | 20-11-03 21:05:4 |", + "00000060 32 7D 5B 26 67 | 2}[&g |", ] diff --git a/macbrew-ui/MacBrewUI.rsrc.bin b/macbrew-ui/MacBrewUI.rsrc.bin index 67a0dc4f4a0c40759bb63b7844bb7a110e760310..01b751b7aa251c0d8cdd0ed63900eb13a52deb1e 100644 GIT binary patch delta 163 zcmZqBYS5Yx&vG{?J9DC-I7?&WY0HguB|_|a3{H$ujQb}46gtmpwdV(W)a0|m?y(UH z3=9GcAkfI*qrf2eAIxq5lXeVNyYB$SlNlHUbAjw93=ItIK$$qG8WEuQWH7stK?lg* l0u^@vnsX7vo?Iv*GI^qiKM!|sYH@L9eqLB+YWYM#1puI|Cf5J} delta 163 zcmZqBYS5Yx&vIuX%_*-D6V} z7#IW?K%kK!LV-c>KZBSd4?_c3#12e0GKerR24Gio+Iu6QK0E$lrvl|&Kfb1<$ maUY;L7a7EiEhZO=i12U+rxq7y=I8lj7MDzJ74e@ar~m*Q4response->data, reader->cursor); + char value; + HLock(reader->response->data); + value = GetCharFromBuffer(*reader->response->data, reader->cursor); reader->cursor += 1; //sizeof(Boolean); *outBoolean = value; + HUnlock(reader->response->data); +} + +static void ReadChar(ResponseReader *reader, char *outChar) +{ + char value; + HLock(reader->response->data); + value = GetCharFromBuffer(*reader->response->data, reader->cursor); + reader->cursor += sizeof(char); + *outChar = value; + HUnlock(reader->response->data); +} + +static void ReadUnsignedChar(ResponseReader *reader, unsigned char *outChar) +{ + unsigned char value; + HLock(reader->response->data); + value = GetCharFromBuffer(*reader->response->data, reader->cursor); + reader->cursor += sizeof(unsigned char); + *outChar = value; + HUnlock(reader->response->data); } static void ReadUnsignedShort(ResponseReader *reader, unsigned short *outShort) { - unsigned short value = GetShortFromBuffer(*reader->response->data, reader->cursor); + unsigned short value; + HLock(reader->response->data); + value = GetShortFromBuffer(*reader->response->data, reader->cursor); reader->cursor += sizeof(unsigned short); *outShort = value; + HUnlock(reader->response->data); +} + +static void ReadUnsignedLong(ResponseReader *reader, unsigned long *outUnsignedLong) +{ + unsigned long value; + HLock(reader->response->data); + value = GetLongFromBuffer(*reader->response->data, reader->cursor); + reader->cursor += sizeof(unsigned long); + *outUnsignedLong = value; + HUnlock(reader->response->data); } static void ReadString(ResponseReader *reader, StringHandle *outString) { unsigned short length = 0; - char *buffer = *(reader->response->data); + char *buffer; Str255 pString; ReadUnsignedShort(reader, &length); + HLock(reader->response->data); + buffer = *(reader->response->data); + if (length > 254) { Panic("\pCannot read strings larger than 254 characters at this time"); @@ -73,8 +115,9 @@ static void ReadString(ResponseReader *reader, StringHandle *outString) memcpy(&pString[1], (char *)buffer + reader->cursor, length); *outString = NewString(pString); - reader->cursor += length; + + HUnlock(reader->response->data); } static void ReadSequence(ResponseReader *reader, Sequence *outSequence) @@ -170,13 +213,18 @@ void FetchBrewSessionReferences(Sequence **outSessionReferences) DisposeResponse(&responseData); } -void FetchBrewSession(StringHandle sessionId, BrewSession **outBrewSession) +void FetchBrewSession(StringHandle sessionId, BrewSessionHandle *outHandle) { Str255 command; Str255 cSessionId; SerialResponse *responseData; ResponseReader reader; - BrewSession *brewSession = (BrewSession *)NewPtr(sizeof(BrewSession)); + Handle handle = NewHandle(sizeof(BrewSession)); + BrewSession *brewSession; + + HLock(handle); + + brewSession = (BrewSession *)*handle; HLock((Handle)sessionId); PascalToCStringCopy(cSessionId, *sessionId); @@ -198,10 +246,79 @@ void FetchBrewSession(StringHandle sessionId, BrewSession **outBrewSession) ReadString(&reader, &brewSession->batch_code); ReadString(&reader, &brewSession->recipe_title); ReadString(&reader, &brewSession->recipe_id); + ReadString(&reader, &brewSession->style_name); + ReadString(&reader, &brewSession->created_at); AssertReaderEnd(&reader, responseData); - *outBrewSession = brewSession; + DisposeResponse(&responseData); + + HUnlock(handle); + + *outHandle = (BrewSessionHandle)handle; +} + +static void ReadFermentationPoint(ResponseReader *reader, Handle *outHandle) +{ + Handle handle = NewHandle(sizeof(FermentationPoint)); + FermentationPoint *fermentationPointReference; + + HLock(handle); + + fermentationPointReference = (FermentationPoint *)*handle; + + ReadChar(reader, &fermentationPointReference->temp); + ReadUnsignedChar(reader, &fermentationPointReference->gravity); + + HUnlock(handle); + + *outHandle = handle; +} + +void FetchFermentationData(StringHandle sessionId, FermentationDataHandle *outHandle) +{ + Str255 command; + Str255 cSessionId; + SerialResponse *responseData; + ResponseReader reader; + Sequence *graph = (Sequence *)NewPtr(sizeof(Sequence)); + short i; + Handle handle = NewHandle(sizeof(FermentationData)); + FermentationData *fermentationData; + + HLock(handle); + + fermentationData = (FermentationData *)*handle; + + HLock((Handle)sessionId); + PascalToCStringCopy(cSessionId, *sessionId); + HUnlock((Handle)sessionId); + + sprintf((char *)command, "1 GET FERMENTATION %s\r", cSessionId); + + SetUpSerial(); + SendCommand((char *)command); + ReadResponse(&responseData); + TearDownSerial(); + + InitReader(&reader, responseData); + + ValidateResponse(&reader); + + ReadSequence(&reader, graph); + for (i = 0; i < graph->size; i++) + { + ReadFermentationPoint(&reader, &graph->elements[i]); + } + fermentationData->graph = graph; + ReadUnsignedLong(&reader, &fermentationData->start_date); + ReadUnsignedLong(&reader, &fermentationData->end_date); + + AssertReaderEnd(&reader, responseData); DisposeResponse(&responseData); + + HUnlock(handle); + + *outHandle = (FermentationDataHandle)handle; } diff --git a/macbrew-ui/mbDataManager.h b/macbrew-ui/mbDataManager.h index f69a25a..1794ef0 100644 --- a/macbrew-ui/mbDataManager.h +++ b/macbrew-ui/mbDataManager.h @@ -1,7 +1,9 @@ struct Sequence; struct SerialResponse; struct BrewSession; +struct FermentationData; void Ping(void); void FetchBrewSessionReferences(struct Sequence **outSessionReferences); -void FetchBrewSession(StringHandle sessionId, struct BrewSession **outBrewSession); +void FetchBrewSession(StringHandle sessionId, struct BrewSession ***outHandle); +void FetchFermentationData(StringHandle sessionId, struct FermentationData ***outHandle); diff --git a/macbrew-ui/mbMenus.c b/macbrew-ui/mbMenus.c index 8f1511a..6bcc52d 100644 --- a/macbrew-ui/mbMenus.c +++ b/macbrew-ui/mbMenus.c @@ -62,6 +62,7 @@ void HandleMenu(long mSelect) WindowPtr sessionListDialog = SessionListDialogSetUp(); short selectedItem; BrewSessionReferenceHandle selectedSession; + FermentationDataHandle fermentationData; BrewSessionHandle brewSession; WindowPtr viewSessionWindow; MakeCursorBusy(); @@ -76,11 +77,14 @@ void HandleMenu(long mSelect) SessionListDialogDestroy(sessionListDialog); selectedSession = (BrewSessionReferenceHandle)sessionReferences->elements[selectedItem]; HLock((Handle)selectedSession); - FetchBrewSession((*selectedSession)->id, brewSession); + FetchBrewSession((*selectedSession)->id, &brewSession); HUnlock((Handle)selectedSession); viewSessionWindow = SessionViewWindowSetUp(); SessionViewSetSession(viewSessionWindow, brewSession); + + FetchFermentationData((*selectedSession)->id, &fermentationData); + SessionViewSetFermentationData(viewSessionWindow, fermentationData); break; } case quitItem: diff --git a/macbrew-ui/mbTypes.h b/macbrew-ui/mbTypes.h index b1b6041..b126d55 100644 --- a/macbrew-ui/mbTypes.h +++ b/macbrew-ui/mbTypes.h @@ -1,3 +1,5 @@ +typedef unsigned long MacEpochTime; + typedef struct Sequence { unsigned short size; @@ -18,8 +20,23 @@ typedef struct BrewSession StringHandle batch_code; StringHandle recipe_title; StringHandle recipe_id; + StringHandle style_name; + StringHandle created_at; } BrewSession; +typedef struct FermentationPoint +{ + char temp; + unsigned char gravity; +} FermentationPoint; + +typedef struct FermentationData +{ + Sequence *graph; + MacEpochTime start_date; + MacEpochTime end_date; +} FermentationData; + typedef struct Fermentable { StringHandle name; @@ -51,3 +68,4 @@ typedef struct Recipe typedef BrewSessionReference **BrewSessionReferenceHandle; typedef BrewSession **BrewSessionHandle; +typedef FermentationData **FermentationDataHandle; diff --git a/macbrew-ui/mbWViewSession.c b/macbrew-ui/mbWViewSession.c index ef88137..ca6e43d 100644 --- a/macbrew-ui/mbWViewSession.c +++ b/macbrew-ui/mbWViewSession.c @@ -1,3 +1,5 @@ +#include + #include "mbConstants.h" #include "mbWViewSession.h" #include "mbTypes.h" @@ -5,11 +7,23 @@ typedef struct ViewSessionWindowState { BrewSessionHandle brewSessionHandle; + FermentationDataHandle fermentationDataHandle; } ViewSessionWindowState; +static void DrawStringHandle(StringHandle stringHandle); static void ViewSessionWindowInitState(WindowPtr theWindow); static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow); static void *ViewSessionWindowUnlockState(WindowPtr theWindow); +static void DrawRow(ConstStr255Param title, StringHandle value, short rowNum); +static void SetupQuickDraw(WindowPtr window); + +// TODO: Maybe move to a utils file? +static void DrawStringHandle(StringHandle stringHandle) +{ + HLock((Handle)stringHandle); + DrawString(*stringHandle); + HUnlock((Handle)stringHandle); +} static void ViewSessionWindowInitState(WindowPtr theWindow) { @@ -33,6 +47,27 @@ static void *ViewSessionWindowUnlockState(WindowPtr theWindow) HUnlock(viewSessionWindowStateHandle); } +static void DrawRow(ConstStr255Param title, StringHandle value, short rowNum) +{ + // TODO: Extract label strings to resources + TextFace(bold); + MoveTo(10, rowNum * 20); + DrawString(title); + + TextFace(normal); + MoveTo(120, rowNum * 20); + DrawStringHandle(value); +} + +static void SetupQuickDraw(WindowPtr window) +{ + // Setup QuickDraw + SetPort(window); + TextFont(geneva); + TextSize(12); + PenNormal(); +} + WindowPtr SessionViewWindowSetUp(void) { WindowPtr viewSessionWindow = NULL; @@ -56,27 +91,105 @@ void SessionViewWindowDestroy(WindowPtr window) void SessionViewSetSession(WindowPtr window, BrewSessionHandle brewSessionHandle) { - // Temporary function to test session select + + unsigned short rowNum = 1; ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); BrewSession *brewSession = NULL; windowState->brewSessionHandle = brewSessionHandle; + HLock((Handle)brewSessionHandle); brewSession = *brewSessionHandle; - HLock((Handle)brewSession->recipe_title); - HLock((Handle)brewSession->phase); + HLock((Handle)brewSession->recipe_title); // Set title to demonstrate its working for now SetWTitle(window, *(brewSession->recipe_title)); + HUnlock((Handle)brewSession->recipe_title); - SetPort(window); - TextFont(geneva); - TextSize(12); + SetupQuickDraw(window); - MoveTo(10, 10); - DrawString(*(brewSession->phase)); + DrawRow("\pCreated At:", brewSession->created_at, rowNum++); + DrawRow("\pBatch Code:", brewSession->batch_code, rowNum++); + DrawRow("\pPhase:", brewSession->phase, rowNum++); + DrawRow("\pStyle:", brewSession->style_name, rowNum++); - HUnlock((Handle)brewSession->phase); - HUnlock((Handle)brewSession->recipe_title); HUnlock((Handle)brewSessionHandle); ViewSessionWindowUnlockState(window); } + +void SessionViewSetFermentationData(WindowPtr window, struct FermentationData **fermentationDataHandle) +{ + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + FermentationData *fermentationData = NULL; + Rect graphFrame; + unsigned short i, pointSize, maxTemp = 0, maxGravity = 0; + + windowState->fermentationDataHandle = fermentationDataHandle; + + SetupQuickDraw(window); + + HLock((Handle)fermentationDataHandle); + fermentationData = *fermentationDataHandle; + + pointSize = kGraphWidth / fermentationData->graph->size; + + graphFrame.top = 100; + graphFrame.left = 20; + graphFrame.right = graphFrame.left + kGraphWidth; + graphFrame.bottom = graphFrame.top + kGraphHeight; + + FrameRect(&graphFrame); + + for (i = 0; i < fermentationData->graph->size; i++) + { + // TODO: These points probably shouldn't be handles + // Should be packed into an array probably + Handle dataPointHandle = fermentationData->graph->elements[i]; + FermentationPoint *dataPoint; + + HLock(dataPointHandle); + dataPoint = (FermentationPoint *)*dataPointHandle; + + if (dataPoint->temp > maxTemp) + { + maxTemp = dataPoint->temp; + } + + if (dataPoint->gravity > maxGravity) + { + maxGravity = dataPoint->gravity; + } + + HUnlock(dataPointHandle); + } + + for (i = 0; i < fermentationData->graph->size - 1; i++) + { + // TODO: These points probably shouldn't be handles + // Should be packed into an array probably + Handle dataPointHandle = fermentationData->graph->elements[i]; + Handle nextDataPointHandle = fermentationData->graph->elements[i + 1]; + FermentationPoint *dataPoint, *nextDataPoint; + unsigned short bottom = graphFrame.top + kGraphHeight; + unsigned short tempScaleFactor = (kGraphHeight - 20) / maxTemp; + unsigned short gravityScaleFactor = (kGraphHeight - 20) / maxGravity; + + HLock(dataPointHandle); + HLock(nextDataPointHandle); + dataPoint = (FermentationPoint *)*dataPointHandle; + nextDataPoint = (FermentationPoint *)*nextDataPointHandle; + + PenNormal(); + MoveTo(graphFrame.left + (pointSize * i), bottom - (dataPoint->temp * tempScaleFactor)); + LineTo(graphFrame.left + (pointSize * (i + 1)), bottom - (nextDataPoint->temp * tempScaleFactor)); + + PenPat(gray); + MoveTo(graphFrame.left + (pointSize * i), bottom - (dataPoint->gravity * gravityScaleFactor)); + LineTo(graphFrame.left + (pointSize * (i + 1)), bottom - (nextDataPoint->gravity * gravityScaleFactor)); + + HUnlock(nextDataPointHandle); + HUnlock(dataPointHandle); + } + + HUnlock((Handle)fermentationDataHandle); + ViewSessionWindowUnlockState(window); +} diff --git a/macbrew-ui/mbWViewSession.h b/macbrew-ui/mbWViewSession.h index cbab018..5163ce2 100644 --- a/macbrew-ui/mbWViewSession.h +++ b/macbrew-ui/mbWViewSession.h @@ -1,6 +1,8 @@ struct BrewSession; +struct FermentationData; WindowPtr SessionViewWindowSetUp(void); void SessionViewWindowDestroy(WindowPtr window); void SessionViewSetSession(WindowPtr window, struct BrewSession **brewSessionHandle); +void SessionViewSetFermentationData(WindowPtr window, struct FermentationData **fermentationDataHandle); From 0b45d3a270452a9adfe8ca9ac1e5e53a96ac70df Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Tue, 31 Aug 2021 10:30:11 +1000 Subject: [PATCH 07/10] feat: implement initial brew steps window --- .vscode/c_cpp_properties.json | 6 +- .vscode/settings.json | 4 +- Images/basilisk_ii_prefs | 2 +- macbrew-proxy/resources/sample_recipe.xml | 12 +- macbrew-proxy/resources/sample_session.json | 6 +- macbrew-proxy/src/commands/list_steps.rs | 224 +++++++++++++ macbrew-proxy/src/commands/mod.rs | 1 + .../src/data/brewers_friend/recipes.rs | 25 +- macbrew-proxy/src/data/macbrew/mod.rs | 1 + macbrew-proxy/src/data/macbrew/recipes.rs | 41 ++- macbrew-proxy/src/data/macbrew/steps.rs | 15 + macbrew-proxy/src/main.rs | 29 ++ macbrew-proxy/src/serializers/mac.rs | 14 +- .../macbrew_proxy__tests__get_recipe.snap | 120 ++++--- .../macbrew_proxy__tests__get_sessions.snap | 10 +- .../macbrew_proxy__tests__list_steps.snap | 102 ++++++ macbrew-ui/MacBrewUI.bin | Bin 12416 -> 12416 bytes macbrew-ui/MacBrewUI.rsrc.bin | Bin 5376 -> 5504 bytes macbrew-ui/macbrew.c | 92 +++++- macbrew-ui/mbConstants.h | 17 + macbrew-ui/mbDSessionList.c | 2 - macbrew-ui/mbDataManager.c | 74 ++++- macbrew-ui/mbDataManager.h | 1 + macbrew-ui/mbDialogUtils.c | 2 + macbrew-ui/mbMenus.c | 45 ++- macbrew-ui/mbTypes.h | 17 + macbrew-ui/mbWViewSession.c | 96 ++++-- macbrew-ui/mbWViewSession.h | 1 + macbrew-ui/mbWViewSteps.c | 299 ++++++++++++++++++ macbrew-ui/mbWViewSteps.h | 9 + 30 files changed, 1157 insertions(+), 110 deletions(-) create mode 100644 macbrew-proxy/src/commands/list_steps.rs create mode 100644 macbrew-proxy/src/data/macbrew/steps.rs create mode 100644 macbrew-proxy/src/snapshots/macbrew_proxy__tests__list_steps.snap create mode 100644 macbrew-ui/mbWViewSteps.c create mode 100644 macbrew-ui/mbWViewSteps.h diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index b23d22c..6f800fa 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -12,10 +12,12 @@ "${myDefaultIncludePath}" ], "defines": [ - "TARGET_OS_MAC" + "TARGET_OS_MAC", + "OLDROUTINENAMES" ], "forcedInclude": [ - "${workspaceFolder}/include/MacIncludes.h" + "${workspaceFolder}/include/MacIncludes.h", + "${workspaceFolder}/include/CIncludes/Carbon.h" ], "cStandard": "c89" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 597c8b6..fc4fbb1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,7 +36,9 @@ "stdio.h": "c", "textutils.h": "c", "quickdraw.h": "c", - "mbserial.h": "c" + "mbserial.h": "c", + "controldefinitions.h": "c", + "mbutil.h": "c" }, "c-cpp-flylint.clang.enable": false, "c-cpp-flylint.flexelint.enable": false, diff --git a/Images/basilisk_ii_prefs b/Images/basilisk_ii_prefs index fec8159..902789c 100644 --- a/Images/basilisk_ii_prefs +++ b/Images/basilisk_ii_prefs @@ -2,7 +2,7 @@ displaycolordepth 0 disk ./system710-macbrew-dev-with-code.image extfs / screen win/512/384 -seriala /dev/pts/3 +seriala /dev/pts/4 serialb /dev/ttyS1 udptunnel false udpport 6066 diff --git a/macbrew-proxy/resources/sample_recipe.xml b/macbrew-proxy/resources/sample_recipe.xml index 0fed60d..381661d 100644 --- a/macbrew-proxy/resources/sample_recipe.xml +++ b/macbrew-proxy/resources/sample_recipe.xml @@ -115,6 +115,16 @@ Also trying some Mandarina Bravaria this time and Eclipse (HPA-016) which appare
Pellet
+ + Warrior + 1 + 11.75 + 0.001 + Boil + Boil + +
Pellet
+
Amarillo (8.6 AA) 1 @@ -358,4 +368,4 @@ Also trying some Mandarina Bravaria this time and Eclipse (HPA-016) which appare 10 - \ No newline at end of file + diff --git a/macbrew-proxy/resources/sample_session.json b/macbrew-proxy/resources/sample_session.json index 8a84610..b419525 100644 --- a/macbrew-proxy/resources/sample_session.json +++ b/macbrew-proxy/resources/sample_session.json @@ -5,7 +5,7 @@ { "id": "363597", "loginid": "217955", - "recipeid": "1072961", + "recipeid": "123456", "folder_id": null, "brewstepprofileid": "1", "phase": "All Gone", @@ -69,7 +69,7 @@ "updated": "2020-12-02T10:29:45+00:00" }, "recipe": { - "id": "1072961", + "id": "123456", "recipetoken": "5fa1c626d6cc4", "brewer_author_id": "217955", "title": "Forresty Fruit", @@ -161,4 +161,4 @@ } } ] -} \ No newline at end of file +} diff --git a/macbrew-proxy/src/commands/list_steps.rs b/macbrew-proxy/src/commands/list_steps.rs new file mode 100644 index 0000000..76b26b5 --- /dev/null +++ b/macbrew-proxy/src/commands/list_steps.rs @@ -0,0 +1,224 @@ +use crate::commands::command::{prepare_response, Command}; +use crate::data::brewers_friend::bf_data_manager::BfDataManager; +use crate::data::macbrew::recipes::Recipe; +use crate::data::macbrew::steps::{ + BrewSessionStep, BOIL_PHASE, CHILL_PHASE, CLEANUP_PHASE, MASH_PHASE, PREPARE_PHASE, +}; +use crate::error::Error::InvalidCommandInput; +use crate::error::Result; +use async_trait::async_trait; +use std::marker::PhantomData; +use std::str::FromStr; + +// TODO: Work out why rustc thinks T is unused. Is PhantomData needed? Can I restructure this? +// Clue: https://github.com/rust-lang/rust/issues/23246 +pub struct ListStepsCommand(PhantomData); + +#[async_trait] +impl Command for ListStepsCommand { + async fn handle(rid: &str, args: &[&str]) -> Result> { + // TODO: This is extremely rough and just a MVP implmentation to start working on the Mac UI + // It will be cleaned up soon + match args { + [session_id] => { + let session = DataManager::session(session_id).await?; + let beer_xml_recipe = DataManager::recipe(&session.recipeid).await?; + let recipe = Recipe::from_beer_xml(&beer_xml_recipe); + + let initial_steps = vec![ + BrewSessionStep { + description: "Check ingredients (hops/yeast etc.) and reset equipment" + .to_string(), + time: -1, + phase: PREPARE_PHASE, + }, + BrewSessionStep { + description: "Prepare strike water (add salt/acid)".to_string(), + time: -1, + phase: PREPARE_PHASE, + }, + ]; + + let mash_hops = recipe + .hops + .iter() + .filter(|h| h.hop_use == "Mash") + .collect::>(); + + let mash_hop_steps: Vec = if mash_hops.is_empty() { + vec![] + } else { + mash_hops + .iter() + .map(|s| { + // TODO: Clean up this mess + let coerced_time = i16::from_str(&s.time).unwrap_or(0); + BrewSessionStep { + description: format!( + "Add {:?}g of {:?} hops to mash at minute {:?}", + s.amount, s.name, s.time, + ), + time: coerced_time, + phase: MASH_PHASE, + } + }) + .collect() + }; + + let mash_steps = recipe + .mash + .mash_steps + .iter() + .map(|s| { + // TODO: Clean up this mess + let time: String = s.step_time.clone().unwrap_or_else(|| "".to_string()); + let coerced_time = i16::from_str(&time).unwrap_or(0); + BrewSessionStep { + description: format!( + "Heat {:?} water to {:?} degrees", + s.mash_step_type, + s.step_temp.clone().unwrap_or_else(|| "unknown".to_string()) + ), + time: coerced_time, + phase: MASH_PHASE, + } + }) + .collect::>(); + + let preboil_steps = vec![ + BrewSessionStep { + description: "Take gravity sample and record pre-boil volume".to_string(), + time: -1, + phase: MASH_PHASE, + }, + BrewSessionStep { + description: "Bring to a boil".to_string(), + time: -1, + phase: MASH_PHASE, + }, + ]; + + let boil_hops = recipe + .hops + .iter() + .filter(|h| h.hop_use == "Boil") + .collect::>(); + + let boil_hop_steps: Vec = if boil_hops.is_empty() { + vec![] + } else { + boil_hops + .iter() + .map(|s| { + // TODO: Clean up this mess + let coerced_time = i16::from_str(&s.time).unwrap_or(0); + BrewSessionStep { + description: format!( + "Add {:?}g of {:?} hops to boil at minute {:?}", + s.amount, s.name, s.time, + ), + time: coerced_time, + phase: BOIL_PHASE, + } + }) + .collect() + }; + + let postboil_steps = vec![ + BrewSessionStep { + description: "Turn off kettle".to_string(), + time: i16::from_str(&recipe.boil_time).unwrap_or(0), + phase: BOIL_PHASE, + }, + BrewSessionStep { + description: "Start chilling wort".to_string(), + time: -1, + phase: BOIL_PHASE, + }, + ]; + + let whirlpool_hops = recipe + .hops + .iter() + .filter(|h| { + // TODO: Maybe move some of this coersion to where the XML recipe is convered to rust objects + h.hop_use == "Aroma" + && h.user_hop_use.clone().unwrap_or_else(|| "".to_string()) + == "Whirlpool" + }) + .collect::>(); + + let whirlpool_hop_steps: Vec = if whirlpool_hops.is_empty() { + vec![] + } else { + whirlpool_hops + .iter() + .map(|s| + BrewSessionStep { + description: format!( + "When temp is {:?} degrees, add {:?}g of {:?} hops to whirlpool for {:?} minutes", + s.hop_temp, s.amount, s.name, s.time + ), + time: -1, + phase: CHILL_PHASE, + } + ) + .collect() + }; + + let chill_steps = vec![ + BrewSessionStep { + description: format!("Cool wort down to {:?}", recipe.primary_temp), + time: -1, + phase: CHILL_PHASE, + }, + BrewSessionStep { + description: "Take gravity reading".to_string(), + time: -1, + phase: CHILL_PHASE, + }, + BrewSessionStep { + description: "Transfer to fermentor".to_string(), + time: -1, + phase: CHILL_PHASE, + }, + BrewSessionStep { + description: "Pitch yeast".to_string(), + time: -1, + phase: CHILL_PHASE, + }, + ]; + + let cleanup_steps = vec![ + BrewSessionStep { + description: "Set fermentation chamber temp".to_string(), + time: -1, + phase: CLEANUP_PHASE, + }, + BrewSessionStep { + description: "Cleanup equipment".to_string(), + time: -1, + phase: CLEANUP_PHASE, + }, + ]; + + let all_steps: Vec = [ + initial_steps, + mash_hop_steps, + mash_steps, + preboil_steps, + boil_hop_steps, + postboil_steps, + whirlpool_hop_steps, + chill_steps, + cleanup_steps, + ] + .concat(); + prepare_response(rid, true, &all_steps) + } + [_args @ ..] => Err(InvalidCommandInput { + message: String::from("Expected single parameter (Session ID)"), + }), + } + } +} diff --git a/macbrew-proxy/src/commands/mod.rs b/macbrew-proxy/src/commands/mod.rs index 655126f..a126591 100644 --- a/macbrew-proxy/src/commands/mod.rs +++ b/macbrew-proxy/src/commands/mod.rs @@ -3,4 +3,5 @@ pub mod get_fermentation; pub mod get_recipes; pub mod get_sessions; pub mod list_sessions; +pub mod list_steps; pub mod ping; diff --git a/macbrew-proxy/src/data/brewers_friend/recipes.rs b/macbrew-proxy/src/data/brewers_friend/recipes.rs index c7828af..d50b641 100644 --- a/macbrew-proxy/src/data/brewers_friend/recipes.rs +++ b/macbrew-proxy/src/data/brewers_friend/recipes.rs @@ -91,6 +91,29 @@ pub struct BeerXmlYeasts { pub YEAST: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[allow(non_snake_case)] +pub struct BeerXmlMashStep { + pub NAME: String, + pub TYPE: String, + pub STEP_TIME: Option, + pub INFUSE_AMOUNT: Option, + pub STEP_TEMP: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[allow(non_snake_case)] +pub struct BeerXmlMashSteps { + pub MASH_STEP: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[allow(non_snake_case)] +pub struct BeerXmlMash { + pub GRAIN_TEMP: String, + pub MASH_STEPS: BeerXmlMashSteps, +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[allow(non_snake_case)] pub struct BeerXmlRecipe { @@ -110,7 +133,7 @@ pub struct BeerXmlRecipe { //pub MISCS: TODO pub YEASTS: BeerXmlYeasts, //pub WATERS: TODO - //pub MASH: TODO + pub MASH: BeerXmlMash, pub NOTES: Option, pub TASTE_NOTES: Option, pub TASTE_RATING: Option, diff --git a/macbrew-proxy/src/data/macbrew/mod.rs b/macbrew-proxy/src/data/macbrew/mod.rs index ccf8d4b..d94d8fe 100644 --- a/macbrew-proxy/src/data/macbrew/mod.rs +++ b/macbrew-proxy/src/data/macbrew/mod.rs @@ -2,3 +2,4 @@ pub mod fermentation; pub mod recipes; pub mod sessions; pub mod shared; +pub mod steps; diff --git a/macbrew-proxy/src/data/macbrew/recipes.rs b/macbrew-proxy/src/data/macbrew/recipes.rs index faf9815..03f3bfd 100644 --- a/macbrew-proxy/src/data/macbrew/recipes.rs +++ b/macbrew-proxy/src/data/macbrew/recipes.rs @@ -11,8 +11,11 @@ pub struct Fermentable { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Hop { pub name: String, - pub amount: String, + pub amount: u16, pub time: String, + pub hop_use: String, + pub user_hop_use: Option, + pub hop_temp: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -21,6 +24,21 @@ pub struct Yeast { pub amount: String, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MashStep { + pub name: String, + pub mash_step_type: String, + pub step_time: Option, + pub infuse_amount: Option, + pub step_temp: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Mash { + pub grain_temp: String, + pub mash_steps: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Recipe { pub name: String, @@ -29,6 +47,9 @@ pub struct Recipe { pub fermentables: Vec, pub hops: Vec, pub yeast: Vec, + pub mash: Mash, + pub boil_time: String, + pub primary_temp: Option, } impl Recipe { @@ -40,13 +61,23 @@ impl Recipe { }); let hops = recipe.HOPS.HOP.iter().map(|r| Hop { name: r.NAME.clone(), - amount: r.AMOUNT.clone(), + amount: (r.AMOUNT.clone().parse::().unwrap_or(0f64) * 1000f64) as u16, // Brewers friend uses KG, we use G time: r.TIME.clone(), + hop_use: r.USE.clone(), + user_hop_use: r.USER_HOP_USE.clone(), + hop_temp: r.HOP_TEMP.clone(), }); let yeast = recipe.YEASTS.YEAST.iter().map(|r| Yeast { name: r.NAME.clone(), amount: r.AMOUNT.clone(), }); + let mash_steps = recipe.MASH.MASH_STEPS.MASH_STEP.iter().map(|r| MashStep { + name: r.NAME.clone(), + mash_step_type: r.TYPE.clone(), + step_time: r.STEP_TIME.clone(), + infuse_amount: r.INFUSE_AMOUNT.clone(), + step_temp: r.STEP_TEMP.clone(), + }); Recipe { name: recipe.NAME.clone(), version: recipe.VERSION.clone(), @@ -54,6 +85,12 @@ impl Recipe { fermentables: fermentables.collect(), hops: hops.collect(), yeast: yeast.collect(), + mash: Mash { + grain_temp: recipe.MASH.GRAIN_TEMP.clone(), + mash_steps: mash_steps.collect(), + }, + boil_time: recipe.BOIL_TIME.clone(), + primary_temp: recipe.PRIMARY_TEMP.clone(), } } } diff --git a/macbrew-proxy/src/data/macbrew/steps.rs b/macbrew-proxy/src/data/macbrew/steps.rs new file mode 100644 index 0000000..4910999 --- /dev/null +++ b/macbrew-proxy/src/data/macbrew/steps.rs @@ -0,0 +1,15 @@ +use serde::Serialize; + +pub const PREPARE_PHASE: u8 = 16; +pub const MASH_PHASE: u8 = 18; +pub const BOIL_PHASE: u8 = 20; +pub const CHILL_PHASE: u8 = 22; +pub const CLEANUP_PHASE: u8 = 24; + +#[repr(C)] +#[derive(Serialize, Debug, Clone)] +pub struct BrewSessionStep { + pub description: String, + pub time: i16, + pub phase: u8, +} diff --git a/macbrew-proxy/src/main.rs b/macbrew-proxy/src/main.rs index fb72c86..e1ed05c 100644 --- a/macbrew-proxy/src/main.rs +++ b/macbrew-proxy/src/main.rs @@ -57,6 +57,10 @@ async fn handle_line(line: &str) -> Result> { ) .await } + ["LIST", "STEP", args @ ..] => { + commands::list_steps::ListStepsCommand::::handle(request_id, args) + .await + } ["GET", "RECIPE", args @ ..] => { commands::get_recipes::GetRecipesCommand::::handle( request_id, args, @@ -265,6 +269,31 @@ mod tests { insta::assert_debug_snapshot!("get_fermentation_manual", format_binary_for_snap(&result)); } + #[tokio::test] + async fn test_list_steps() { + let session_json = include_str!("../resources/sample_session.json"); + let _session_mock = mock("GET", "/v1/brewsessions/363597") + .with_status(200) + .with_header("content-type", "application/xml") + .with_header("x-api-key", "1234") + .with_body(session_json) + .create(); + let recipe_xml = include_str!("../resources/sample_recipe.xml"); + let _recipe_mock = mock("GET", "/v1/recipes/123456.xml") + .with_status(200) + .with_header("content-type", "application/xml") + .with_header("x-api-key", "1234") + .with_body(recipe_xml) + .create(); + + let result = handle_line("888 LIST STEP 363597").await.unwrap(); + + assert_length(&result); + assert_checksum(&result); + assert_request_id(&result, "888"); + insta::assert_debug_snapshot!("list_steps", format_binary_for_snap(&result)); + } + #[tokio::test] async fn test_ping() { let result = handle_line("1 PING").await.unwrap(); diff --git a/macbrew-proxy/src/serializers/mac.rs b/macbrew-proxy/src/serializers/mac.rs index 96fd2d6..9ae5bb3 100644 --- a/macbrew-proxy/src/serializers/mac.rs +++ b/macbrew-proxy/src/serializers/mac.rs @@ -162,11 +162,21 @@ impl<'a> ser::Serializer for &'a mut Serializer { } fn serialize_str(self, v: &str) -> Result<()> { + let bytes = v.as_bytes(); + let truncated_bytes: &[u8] = if bytes.len() > 254 { + // TODO: Work out how to support longer strings in Mac land + // in a generic way + println!("Warning: String greater than 254 characters, it will be truncated to 254 characters"); + &bytes[0..254] + } else { + bytes + }; + self.output - .try_extend(&(v.len() as u16).to_be_bytes()) + .try_extend(&(truncated_bytes.len() as u16).to_be_bytes()) .map_err(|_| Error::SerializeBufferFull {})?; self.output - .try_extend(v.as_bytes()) + .try_extend(truncated_bytes) .map_err(|_| Error::SerializeBufferFull {})?; Ok(()) } diff --git a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_recipe.snap b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_recipe.snap index 9a935ec..b9a4e0c 100644 --- a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_recipe.snap +++ b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_recipe.snap @@ -3,60 +3,84 @@ source: macbrew-proxy/src/main.rs expression: format_binary_for_snap(&result) --- [ - "00000000 03 6E 00 02 31 31 01 00 0E 46 6F 72 72 65 73 74 | ♥n.☻11☺.♫Forrest |", + "00000000 04 F5 00 02 31 31 01 00 0E 46 6F 72 72 65 73 74 | ♦⌡.☻11☺.♫Forrest |", "00000010 79 20 46 72 75 69 74 00 01 31 00 09 41 6C 6C 20 | y Fruit.☺1.○All |", "00000020 47 72 61 69 6E 00 03 00 10 4D 61 72 69 73 20 4F | Grain.♥.►Maris O |", "00000030 74 74 65 72 20 70 61 6C 65 00 01 34 00 0B 42 45 | tter pale.☺4.♂BE |", "00000040 53 54 20 4D 75 6E 69 63 68 00 04 30 2E 32 35 00 | ST Munich.♦0.25. |", "00000050 14 4C 61 63 74 6F 73 65 20 28 4D 69 6C 6B 20 53 | ¶Lactose (Milk S |", - "00000060 75 67 61 72 29 00 03 30 2E 32 00 0C 00 36 41 6D | ugar).♥0.2.♀.6Am |", + "00000060 75 67 61 72 29 00 03 30 2E 32 00 0D 00 36 41 6D | ugar).♥0.2.♪.6Am |", "00000070 61 72 69 6C 6C 6F 20 28 38 2E 36 20 41 41 29 0D | arillo (8.6 AA)♪ |", "00000080 0A 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | ◙ |", "00000090 20 20 20 20 20 20 20 20 20 2F 20 31 34 30 20 47 | / 140 G |", - "000000A0 72 61 6D 73 00 04 30 2E 30 31 00 01 30 00 3D 48 | rams.♦0.01.☺0.=H |", - "000000B0 50 41 20 2D 20 20 45 63 6C 69 70 73 65 20 28 31 | PA - Eclipse (1 |", - "000000C0 36 2E 39 20 41 41 29 0D 0A 20 20 20 20 20 20 20 | 6.9 AA)♪◙ |", - "000000D0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", - "000000E0 20 2F 20 31 30 30 20 47 72 61 6D 73 00 05 30 2E | / 100 Grams.♣0. |", - "000000F0 30 30 35 00 01 30 00 35 4C 65 6D 6F 6E 64 72 6F | 005.☺0.5Lemondro |", - "00000100 70 20 28 36 20 41 41 29 0D 0A 20 20 20 20 20 20 | p (6 AA)♪◙ |", - "00000110 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", - "00000120 20 20 2F 20 31 30 30 20 47 72 61 6D 73 00 04 30 | / 100 Grams.♦0 |", - "00000130 2E 30 32 00 01 30 00 3F 4D 61 6E 64 61 72 69 6E | .02.☺0.?Mandarin |", - "00000140 61 20 42 61 76 61 72 69 61 20 28 38 2E 35 20 41 | a Bavaria (8.5 A |", - "00000150 41 29 0D 0A 20 20 20 20 20 20 20 20 20 20 20 20 | A)♪◙ |", - "00000160 20 20 20 20 20 20 20 20 20 20 20 20 2F 20 31 30 | / 10 |", - "00000170 30 20 47 72 61 6D 73 00 05 30 2E 30 30 35 00 01 | 0 Grams.♣0.005.☺ |", - "00000180 30 00 11 41 6D 61 72 69 6C 6C 6F 20 28 38 2E 36 | 0.◄Amarillo (8.6 |", - "00000190 20 41 41 29 00 04 30 2E 30 31 00 02 31 35 00 3D | AA).♦0.01.☻15.= |", - "000001A0 48 50 41 20 2D 20 20 45 63 6C 69 70 73 65 20 28 | HPA - Eclipse ( |", - "000001B0 31 36 2E 39 20 41 41 29 0D 0A 20 20 20 20 20 20 | 16.9 AA)♪◙ |", - "000001C0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", - "000001D0 20 20 2F 20 31 30 30 20 47 72 61 6D 73 00 04 30 | / 100 Grams.♦0 |", - "000001E0 2E 30 31 00 02 31 35 00 10 4C 65 6D 6F 6E 64 72 | .01.☻15.►Lemondr |", - "000001F0 6F 70 20 28 36 20 41 41 29 00 04 30 2E 30 31 00 | op (6 AA).♦0.01. |", - "00000200 02 31 35 00 3F 4D 61 6E 64 61 72 69 6E 61 20 42 | ☻15.?Mandarina B |", - "00000210 61 76 61 72 69 61 20 28 38 2E 35 20 41 41 29 0D | avaria (8.5 AA)♪ |", - "00000220 0A 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | ◙ |", - "00000230 20 20 20 20 20 20 20 20 20 2F 20 31 30 30 20 47 | / 100 G |", - "00000240 72 61 6D 73 00 04 30 2E 30 31 00 02 31 35 00 36 | rams.♦0.01.☻15.6 |", - "00000250 41 6D 61 72 69 6C 6C 6F 20 28 38 2E 36 20 41 41 | Amarillo (8.6 AA |", - "00000260 29 0D 0A 20 20 20 20 20 20 20 20 20 20 20 20 20 | )♪◙ |", - "00000270 20 20 20 20 20 20 20 20 20 20 20 2F 20 32 38 30 | / 280 |", - "00000280 20 47 72 61 6D 73 00 04 30 2E 30 32 00 04 35 37 | Grams.♦0.02.♦57 |", - "00000290 36 30 00 3D 48 50 41 20 2D 20 20 45 63 6C 69 70 | 60.=HPA - Eclip |", - "000002A0 73 65 20 28 31 36 2E 39 20 41 41 29 0D 0A 20 20 | se (16.9 AA)♪◙ |", - "000002B0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", - "000002C0 20 20 20 20 20 20 2F 20 31 30 30 20 47 72 61 6D | / 100 Gram |", - "000002D0 73 00 04 30 2E 30 31 00 04 35 37 36 30 00 10 4C | s.♦0.01.♦5760.►L |", - "000002E0 65 6D 6F 6E 64 72 6F 70 20 28 36 20 41 41 29 00 | emondrop (6 AA). |", - "000002F0 04 30 2E 30 34 00 04 35 37 36 30 00 3F 4D 61 6E | ♦0.04.♦5760.?Man |", - "00000300 64 61 72 69 6E 61 20 42 61 76 61 72 69 61 20 28 | darina Bavaria ( |", - "00000310 38 2E 35 20 41 41 29 0D 0A 20 20 20 20 20 20 20 | 8.5 AA)♪◙ |", - "00000320 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", - "00000330 20 2F 20 31 30 30 20 47 72 61 6D 73 00 04 30 2E | / 100 Grams.♦0. |", - "00000340 30 31 00 04 35 37 36 30 00 01 00 1E 4C 41 4C 42 | 01.♦5760.☺.▲LALB |", - "00000350 52 45 57 C2 AE 20 56 4F 53 53 20 4B 56 45 49 4B | REW┬« VOSS KVEIK |", - "00000360 20 41 4C 45 20 59 45 41 53 54 00 04 30 2E 31 31 | ALE YEAST.♦0.11 |", - "00000370 68 FF DE 5D | h.▐] |", + "000000A0 72 61 6D 73 00 0A 00 01 30 00 04 4D 61 73 68 01 | rams.◙.☺0.♦Mash☺ |", + "000000B0 00 04 4D 61 73 68 00 00 3D 48 50 41 20 2D 20 20 | .♦Mash..=HPA - |", + "000000C0 45 63 6C 69 70 73 65 20 28 31 36 2E 39 20 41 41 | Eclipse (16.9 AA |", + "000000D0 29 0D 0A 20 20 20 20 20 20 20 20 20 20 20 20 20 | )♪◙ |", + "000000E0 20 20 20 20 20 20 20 20 20 20 20 2F 20 31 30 30 | / 100 |", + "000000F0 20 47 72 61 6D 73 00 05 00 01 30 00 04 4D 61 73 | Grams.♣.☺0.♦Mas |", + "00000100 68 01 00 04 4D 61 73 68 00 00 35 4C 65 6D 6F 6E | h☺.♦Mash..5Lemon |", + "00000110 64 72 6F 70 20 28 36 20 41 41 29 0D 0A 20 20 20 | drop (6 AA)♪◙ |", + "00000120 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "00000130 20 20 20 20 20 2F 20 31 30 30 20 47 72 61 6D 73 | / 100 Grams |", + "00000140 00 14 00 01 30 00 04 4D 61 73 68 01 00 04 4D 61 | .¶.☺0.♦Mash☺.♦Ma |", + "00000150 73 68 00 00 3F 4D 61 6E 64 61 72 69 6E 61 20 42 | sh..?Mandarina B |", + "00000160 61 76 61 72 69 61 20 28 38 2E 35 20 41 41 29 0D | avaria (8.5 AA)♪ |", + "00000170 0A 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | ◙ |", + "00000180 20 20 20 20 20 20 20 20 20 2F 20 31 30 30 20 47 | / 100 G |", + "00000190 72 61 6D 73 00 05 00 01 30 00 04 4D 61 73 68 01 | rams.♣.☺0.♦Mash☺ |", + "000001A0 00 04 4D 61 73 68 00 00 07 57 61 72 72 69 6F 72 | .♦Mash..•Warrior |", + "000001B0 00 01 00 02 31 30 00 04 42 6F 69 6C 01 00 04 42 | .☺.☻10.♦Boil☺.♦B |", + "000001C0 6F 69 6C 00 00 11 41 6D 61 72 69 6C 6C 6F 20 28 | oil..◄Amarillo ( |", + "000001D0 38 2E 36 20 41 41 29 00 0A 00 02 31 35 00 05 41 | 8.6 AA).◙.☻15.♣A |", + "000001E0 72 6F 6D 61 01 00 09 57 68 69 72 6C 70 6F 6F 6C | roma☺.○Whirlpool |", + "000001F0 01 00 02 37 35 00 3D 48 50 41 20 2D 20 20 45 63 | ☺.☻75.=HPA - Ec |", + "00000200 6C 69 70 73 65 20 28 31 36 2E 39 20 41 41 29 0D | lipse (16.9 AA)♪ |", + "00000210 0A 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | ◙ |", + "00000220 20 20 20 20 20 20 20 20 20 2F 20 31 30 30 20 47 | / 100 G |", + "00000230 72 61 6D 73 00 0A 00 02 31 35 00 05 41 72 6F 6D | rams.◙.☻15.♣Arom |", + "00000240 61 01 00 09 57 68 69 72 6C 70 6F 6F 6C 01 00 02 | a☺.○Whirlpool☺.☻ |", + "00000250 37 35 00 10 4C 65 6D 6F 6E 64 72 6F 70 20 28 36 | 75.►Lemondrop (6 |", + "00000260 20 41 41 29 00 0A 00 02 31 35 00 05 41 72 6F 6D | AA).◙.☻15.♣Arom |", + "00000270 61 01 00 09 57 68 69 72 6C 70 6F 6F 6C 01 00 02 | a☺.○Whirlpool☺.☻ |", + "00000280 37 35 00 3F 4D 61 6E 64 61 72 69 6E 61 20 42 61 | 75.?Mandarina Ba |", + "00000290 76 61 72 69 61 20 28 38 2E 35 20 41 41 29 0D 0A | varia (8.5 AA)♪◙ |", + "000002A0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "000002B0 20 20 20 20 20 20 20 20 2F 20 31 30 30 20 47 72 | / 100 Gr |", + "000002C0 61 6D 73 00 0A 00 02 31 35 00 05 41 72 6F 6D 61 | ams.◙.☻15.♣Aroma |", + "000002D0 01 00 09 57 68 69 72 6C 70 6F 6F 6C 01 00 02 37 | ☺.○Whirlpool☺.☻7 |", + "000002E0 35 00 36 41 6D 61 72 69 6C 6C 6F 20 28 38 2E 36 | 5.6Amarillo (8.6 |", + "000002F0 20 41 41 29 0D 0A 20 20 20 20 20 20 20 20 20 20 | AA)♪◙ |", + "00000300 20 20 20 20 20 20 20 20 20 20 20 20 20 20 2F 20 | / |", + "00000310 32 38 30 20 47 72 61 6D 73 00 14 00 04 35 37 36 | 280 Grams.¶.♦576 |", + "00000320 30 00 07 44 72 79 20 48 6F 70 01 00 16 44 72 79 | 0.•Dry Hop☺.▬Dry |", + "00000330 20 48 6F 70 20 28 48 69 67 68 20 4B 72 61 75 73 | Hop (High Kraus |", + "00000340 65 6E 29 00 00 3D 48 50 41 20 2D 20 20 45 63 6C | en)..=HPA - Ecl |", + "00000350 69 70 73 65 20 28 31 36 2E 39 20 41 41 29 0D 0A | ipse (16.9 AA)♪◙ |", + "00000360 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "00000370 20 20 20 20 20 20 20 20 2F 20 31 30 30 20 47 72 | / 100 Gr |", + "00000380 61 6D 73 00 0A 00 04 35 37 36 30 00 07 44 72 79 | ams.◙.♦5760.•Dry |", + "00000390 20 48 6F 70 01 00 16 44 72 79 20 48 6F 70 20 28 | Hop☺.▬Dry Hop ( |", + "000003A0 48 69 67 68 20 4B 72 61 75 73 65 6E 29 00 00 10 | High Krausen)..► |", + "000003B0 4C 65 6D 6F 6E 64 72 6F 70 20 28 36 20 41 41 29 | Lemondrop (6 AA) |", + "000003C0 00 28 00 04 35 37 36 30 00 07 44 72 79 20 48 6F | .(.♦5760.•Dry Ho |", + "000003D0 70 01 00 16 44 72 79 20 48 6F 70 20 28 48 69 67 | p☺.▬Dry Hop (Hig |", + "000003E0 68 20 4B 72 61 75 73 65 6E 29 00 00 3F 4D 61 6E | h Krausen)..?Man |", + "000003F0 64 61 72 69 6E 61 20 42 61 76 61 72 69 61 20 28 | darina Bavaria ( |", + "00000400 38 2E 35 20 41 41 29 0D 0A 20 20 20 20 20 20 20 | 8.5 AA)♪◙ |", + "00000410 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "00000420 20 2F 20 31 30 30 20 47 72 61 6D 73 00 0A 00 04 | / 100 Grams.◙.♦ |", + "00000430 35 37 36 30 00 07 44 72 79 20 48 6F 70 01 00 16 | 5760.•Dry Hop☺.▬ |", + "00000440 44 72 79 20 48 6F 70 20 28 48 69 67 68 20 4B 72 | Dry Hop (High Kr |", + "00000450 61 75 73 65 6E 29 00 00 01 00 1E 4C 41 4C 42 52 | ausen)..☺.▲LALBR |", + "00000460 45 57 C2 AE 20 56 4F 53 53 20 4B 56 45 49 4B 20 | EW┬« VOSS KVEIK |", + "00000470 41 4C 45 20 59 45 41 53 54 00 04 30 2E 31 31 00 | ALE YEAST.♦0.11. |", + "00000480 02 32 30 00 04 00 00 00 06 53 74 72 69 6B 65 01 | ☻20.♦...♠Strike☺ |", + "00000490 00 00 01 00 02 31 36 01 00 02 36 37 00 00 00 08 | ..☺.☻16☺.☻67...◘ |", + "000004A0 49 6E 66 75 73 69 6F 6E 01 00 02 36 30 01 00 00 | Infusion☺.☻60☺.. |", + "000004B0 01 00 02 36 37 00 08 4D 61 73 68 20 6F 75 74 00 | ☺.☻67.◘Mash out. |", + "000004C0 08 49 6E 66 75 73 69 6F 6E 01 00 02 31 30 01 00 | ◘Infusion☺.☻10☺. |", + "000004D0 00 01 00 02 37 35 00 00 00 08 49 6E 66 75 73 69 | .☺.☻75...◘Infusi |", + "000004E0 6F 6E 01 00 00 01 00 01 33 01 00 02 37 35 00 02 | on☺..☺.☺3☺.☻75.☻ |", + "000004F0 31 30 01 00 02 33 37 8B FF 45 17 | 10☺.☻37ï.E↨ |", ] diff --git a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap index 3f2c7fc..41ed48e 100644 --- a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap +++ b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__get_sessions.snap @@ -3,11 +3,11 @@ source: macbrew-proxy/src/main.rs expression: format_binary_for_snap(&result) --- [ - "00000000 00 5F 00 02 33 33 01 00 06 33 36 33 35 39 37 00 | ._.☻33☺.♠363597. |", + "00000000 00 5E 00 02 33 33 01 00 06 33 36 33 35 39 37 00 | .^.☻33☺.♠363597. |", "00000010 08 41 6C 6C 20 47 6F 6E 65 00 05 46 46 30 30 32 | ◘All Gone.♣FF002 |", "00000020 00 0E 46 6F 72 72 65 73 74 79 20 46 72 75 69 74 | .♫Forresty Fruit |", - "00000030 00 07 31 30 37 32 39 36 31 00 11 41 6D 65 72 69 | .•1072961.◄Ameri |", - "00000040 63 61 6E 20 50 61 6C 65 20 41 6C 65 00 13 32 30 | can Pale Ale.‼20 |", - "00000050 32 30 2D 31 31 2D 30 33 20 32 31 3A 30 35 3A 34 | 20-11-03 21:05:4 |", - "00000060 32 7D 5B 26 67 | 2}[&g |", + "00000030 00 06 31 32 33 34 35 36 00 11 41 6D 65 72 69 63 | .♠123456.◄Americ |", + "00000040 61 6E 20 50 61 6C 65 20 41 6C 65 00 13 32 30 32 | an Pale Ale.‼202 |", + "00000050 30 2D 31 31 2D 30 33 20 32 31 3A 30 35 3A 34 32 | 0-11-03 21:05:42 |", + "00000060 6C 32 61 64 | l2ad |", ] diff --git a/macbrew-proxy/src/snapshots/macbrew_proxy__tests__list_steps.snap b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__list_steps.snap new file mode 100644 index 0000000..41925af --- /dev/null +++ b/macbrew-proxy/src/snapshots/macbrew_proxy__tests__list_steps.snap @@ -0,0 +1,102 @@ +--- +source: macbrew-proxy/src/main.rs +expression: format_binary_for_snap(&result) +--- +[ + "00000000 05 F0 00 03 38 38 38 01 00 19 00 37 43 68 65 63 | ♣≡.♥888☺.↓.7Chec |", + "00000010 6B 20 69 6E 67 72 65 64 69 65 6E 74 73 20 28 68 | k ingredients (h |", + "00000020 6F 70 73 2F 79 65 61 73 74 20 65 74 63 2E 29 20 | ops/yeast etc.) |", + "00000030 61 6E 64 20 72 65 73 65 74 20 65 71 75 69 70 6D | and reset equipm |", + "00000040 65 6E 74 FF FF 10 00 24 50 72 65 70 61 72 65 20 | ent..►.$Prepare |", + "00000050 73 74 72 69 6B 65 20 77 61 74 65 72 20 28 61 64 | strike water (ad |", + "00000060 64 20 73 61 6C 74 2F 61 63 69 64 29 FF FF 10 00 | d salt/acid)..►. |", + "00000070 60 41 64 64 20 31 30 67 20 6F 66 20 22 41 6D 61 | `Add 10g of \"Ama |", + "00000080 72 69 6C 6C 6F 20 28 38 2E 36 20 41 41 29 5C 72 | rillo (8.6 AA)\\r |", + "00000090 5C 6E 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | \\n |", + "000000A0 20 20 20 20 20 20 20 20 20 20 2F 20 31 34 30 20 | / 140 |", + "000000B0 47 72 61 6D 73 22 20 68 6F 70 73 20 74 6F 20 6D | Grams\" hops to m |", + "000000C0 61 73 68 20 61 74 20 6D 69 6E 75 74 65 20 22 30 | ash at minute \"0 |", + "000000D0 22 00 00 12 00 66 41 64 64 20 35 67 20 6F 66 20 | \"..↕.fAdd 5g of |", + "000000E0 22 48 50 41 20 2D 20 20 45 63 6C 69 70 73 65 20 | \"HPA - Eclipse |", + "000000F0 28 31 36 2E 39 20 41 41 29 5C 72 5C 6E 20 20 20 | (16.9 AA)\\r\\n |", + "00000100 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "00000110 20 20 20 20 20 2F 20 31 30 30 20 47 72 61 6D 73 | / 100 Grams |", + "00000120 22 20 68 6F 70 73 20 74 6F 20 6D 61 73 68 20 61 | \" hops to mash a |", + "00000130 74 20 6D 69 6E 75 74 65 20 22 30 22 00 00 12 00 | t minute \"0\"..↕. |", + "00000140 5F 41 64 64 20 32 30 67 20 6F 66 20 22 4C 65 6D | _Add 20g of \"Lem |", + "00000150 6F 6E 64 72 6F 70 20 28 36 20 41 41 29 5C 72 5C | ondrop (6 AA)\\r\\ |", + "00000160 6E 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | n |", + "00000170 20 20 20 20 20 20 20 20 20 2F 20 31 30 30 20 47 | / 100 G |", + "00000180 72 61 6D 73 22 20 68 6F 70 73 20 74 6F 20 6D 61 | rams\" hops to ma |", + "00000190 73 68 20 61 74 20 6D 69 6E 75 74 65 20 22 30 22 | sh at minute \"0\" |", + "000001A0 00 00 12 00 68 41 64 64 20 35 67 20 6F 66 20 22 | ..↕.hAdd 5g of \" |", + "000001B0 4D 61 6E 64 61 72 69 6E 61 20 42 61 76 61 72 69 | Mandarina Bavari |", + "000001C0 61 20 28 38 2E 35 20 41 41 29 5C 72 5C 6E 20 20 | a (8.5 AA)\\r\\n |", + "000001D0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "000001E0 20 20 20 20 20 20 2F 20 31 30 30 20 47 72 61 6D | / 100 Gram |", + "000001F0 73 22 20 68 6F 70 73 20 74 6F 20 6D 61 73 68 20 | s\" hops to mash |", + "00000200 61 74 20 6D 69 6E 75 74 65 20 22 30 22 00 00 12 | at minute \"0\"..↕ |", + "00000210 00 23 48 65 61 74 20 22 53 74 72 69 6B 65 22 20 | .#Heat \"Strike\" |", + "00000220 77 61 74 65 72 20 74 6F 20 22 36 37 22 20 64 65 | water to \"67\" de |", + "00000230 67 72 65 65 73 00 00 12 00 25 48 65 61 74 20 22 | grees..↕.%Heat \" |", + "00000240 49 6E 66 75 73 69 6F 6E 22 20 77 61 74 65 72 20 | Infusion\" water |", + "00000250 74 6F 20 22 36 37 22 20 64 65 67 72 65 65 73 00 | to \"67\" degrees. |", + "00000260 3C 12 00 25 48 65 61 74 20 22 49 6E 66 75 73 69 | <↕.%Heat \"Infusi |", + "00000270 6F 6E 22 20 77 61 74 65 72 20 74 6F 20 22 37 35 | on\" water to \"75 |", + "00000280 22 20 64 65 67 72 65 65 73 00 0A 12 00 25 48 65 | \" degrees.◙↕.%He |", + "00000290 61 74 20 22 49 6E 66 75 73 69 6F 6E 22 20 77 61 | at \"Infusion\" wa |", + "000002A0 74 65 72 20 74 6F 20 22 37 35 22 20 64 65 67 72 | ter to \"75\" degr |", + "000002B0 65 65 73 00 00 12 00 2E 54 61 6B 65 20 67 72 61 | ees..↕..Take gra |", + "000002C0 76 69 74 79 20 73 61 6D 70 6C 65 20 61 6E 64 20 | vity sample and |", + "000002D0 72 65 63 6F 72 64 20 70 72 65 2D 62 6F 69 6C 20 | record pre-boil |", + "000002E0 76 6F 6C 75 6D 65 FF FF 12 00 0F 42 72 69 6E 67 | volume..↕.☼Bring |", + "000002F0 20 74 6F 20 61 20 62 6F 69 6C FF FF 12 00 2F 41 | to a boil..↕./A |", + "00000300 64 64 20 31 67 20 6F 66 20 22 57 61 72 72 69 6F | dd 1g of \"Warrio |", + "00000310 72 22 20 68 6F 70 73 20 74 6F 20 62 6F 69 6C 20 | r\" hops to boil |", + "00000320 61 74 20 6D 69 6E 75 74 65 20 22 31 30 22 00 0A | at minute \"10\".◙ |", + "00000330 14 00 0F 54 75 72 6E 20 6F 66 66 20 6B 65 74 74 | ¶.☼Turn off kett |", + "00000340 6C 65 00 0A 14 00 13 53 74 61 72 74 20 63 68 69 | le.◙¶.‼Start chi |", + "00000350 6C 6C 69 6E 67 20 77 6F 72 74 FF FF 14 00 62 57 | lling wort..¶.bW |", + "00000360 68 65 6E 20 74 65 6D 70 20 69 73 20 53 6F 6D 65 | hen temp is Some |", + "00000370 28 22 37 35 22 29 20 64 65 67 72 65 65 73 2C 20 | (\"75\") degrees, |", + "00000380 61 64 64 20 31 30 67 20 6F 66 20 22 41 6D 61 72 | add 10g of \"Amar |", + "00000390 69 6C 6C 6F 20 28 38 2E 36 20 41 41 29 22 20 68 | illo (8.6 AA)\" h |", + "000003A0 6F 70 73 20 74 6F 20 77 68 69 72 6C 70 6F 6F 6C | ops to whirlpool |", + "000003B0 20 66 6F 72 20 22 31 35 22 20 6D 69 6E 75 74 65 | for \"15\" minute |", + "000003C0 73 FF FF 16 00 90 57 68 65 6E 20 74 65 6D 70 20 | s..▬.ÉWhen temp |", + "000003D0 69 73 20 53 6F 6D 65 28 22 37 35 22 29 20 64 65 | is Some(\"75\") de |", + "000003E0 67 72 65 65 73 2C 20 61 64 64 20 31 30 67 20 6F | grees, add 10g o |", + "000003F0 66 20 22 48 50 41 20 2D 20 20 45 63 6C 69 70 73 | f \"HPA - Eclips |", + "00000400 65 20 28 31 36 2E 39 20 41 41 29 5C 72 5C 6E 20 | e (16.9 AA)\\r\\n |", + "00000410 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "00000420 20 20 20 20 20 20 20 2F 20 31 30 30 20 47 72 61 | / 100 Gra |", + "00000430 6D 73 22 20 68 6F 70 73 20 74 6F 20 77 68 69 72 | ms\" hops to whir |", + "00000440 6C 70 6F 6F 6C 20 66 6F 72 20 22 31 35 22 20 6D | lpool for \"15\" m |", + "00000450 69 6E 75 74 65 73 FF FF 16 00 61 57 68 65 6E 20 | inutes..▬.aWhen |", + "00000460 74 65 6D 70 20 69 73 20 53 6F 6D 65 28 22 37 35 | temp is Some(\"75 |", + "00000470 22 29 20 64 65 67 72 65 65 73 2C 20 61 64 64 20 | \") degrees, add |", + "00000480 31 30 67 20 6F 66 20 22 4C 65 6D 6F 6E 64 72 6F | 10g of \"Lemondro |", + "00000490 70 20 28 36 20 41 41 29 22 20 68 6F 70 73 20 74 | p (6 AA)\" hops t |", + "000004A0 6F 20 77 68 69 72 6C 70 6F 6F 6C 20 66 6F 72 20 | o whirlpool for |", + "000004B0 22 31 35 22 20 6D 69 6E 75 74 65 73 FF FF 16 00 | \"15\" minutes..▬. |", + "000004C0 92 57 68 65 6E 20 74 65 6D 70 20 69 73 20 53 6F | ÆWhen temp is So |", + "000004D0 6D 65 28 22 37 35 22 29 20 64 65 67 72 65 65 73 | me(\"75\") degrees |", + "000004E0 2C 20 61 64 64 20 31 30 67 20 6F 66 20 22 4D 61 | , add 10g of \"Ma |", + "000004F0 6E 64 61 72 69 6E 61 20 42 61 76 61 72 69 61 20 | ndarina Bavaria |", + "00000500 28 38 2E 35 20 41 41 29 5C 72 5C 6E 20 20 20 20 | (8.5 AA)\\r\\n |", + "00000510 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |", + "00000520 20 20 20 20 2F 20 31 30 30 20 47 72 61 6D 73 22 | / 100 Grams\" |", + "00000530 20 68 6F 70 73 20 74 6F 20 77 68 69 72 6C 70 6F | hops to whirlpo |", + "00000540 6F 6C 20 66 6F 72 20 22 31 35 22 20 6D 69 6E 75 | ol for \"15\" minu |", + "00000550 74 65 73 FF FF 16 00 1C 43 6F 6F 6C 20 77 6F 72 | tes..▬.∟Cool wor |", + "00000560 74 20 64 6F 77 6E 20 74 6F 20 53 6F 6D 65 28 22 | t down to Some(\" |", + "00000570 33 37 22 29 FF FF 16 00 14 54 61 6B 65 20 67 72 | 37\")..▬.¶Take gr |", + "00000580 61 76 69 74 79 20 72 65 61 64 69 6E 67 FF FF 16 | avity reading..▬ |", + "00000590 00 15 54 72 61 6E 73 66 65 72 20 74 6F 20 66 65 | .§Transfer to fe |", + "000005A0 72 6D 65 6E 74 6F 72 FF FF 16 00 0B 50 69 74 63 | rmentor..▬.♂Pitc |", + "000005B0 68 20 79 65 61 73 74 FF FF 16 00 1D 53 65 74 20 | h yeast..▬.↔Set |", + "000005C0 66 65 72 6D 65 6E 74 61 74 69 6F 6E 20 63 68 61 | fermentation cha |", + "000005D0 6D 62 65 72 20 74 65 6D 70 FF FF 18 00 11 43 6C | mber temp..↑.◄Cl |", + "000005E0 65 61 6E 75 70 20 65 71 75 69 70 6D 65 6E 74 FF | eanup equipment. |", + "000005F0 FF 18 96 81 9B EF | .↑ûüø∩ |", +] diff --git a/macbrew-ui/MacBrewUI.bin b/macbrew-ui/MacBrewUI.bin index d35218895f93e23b203a5a7f7a3070f7feaa83e4..990e08568530ec5be49d676dd36cb4b3ca1e9337 100644 GIT binary patch delta 3061 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 delta 145 zcmZqBZqS+##hD=JwPexVpzO?vg5qM0ji)UcfPfK*h0Pflgn*b)bK-portRect, limitRect; + long growSize; + + SetRect(&limitRect, kMinWindowSize, kMinWindowSize, kMaxWindowSize, kMaxWindowSize); + + // Track the grow button action + growSize = GrowWindow(thisWindow, event->where, &limitRect); + + if (growSize != 0) + { + Rect newViewRect; + // Do the actual resize + SizeWindow(thisWindow, LoWord(growSize), HiWord(growSize), TRUE); + + // Invalidate window + if (windowKind == kViewStepsWindowId) + { + StepsViewHandleGrow(event, thisWindow); + } + + // TODO: Work out how to validate the part of the window that hasn't changed (see docs) + // without breaking the grow button rendering + } +} + static void HandleMouseDown(EventRecord *theEvent) { WindowPtr theWindow; @@ -55,16 +99,16 @@ static void HandleMouseDown(EventRecord *theEvent) { SelectWindow(theWindow); } - else - { - InvalRect(&theWindow->portRect); - } if (windowKind == kSplashWindowId) { // Close the splash screen if clicked DestroySplashWindow(theWindow); } + else if (windowKind == kViewStepsWindowId) + { + StepsViewHandleMouseDown(theEvent, theWindow); + } break; case inGoAway: @@ -74,6 +118,19 @@ static void HandleMouseDown(EventRecord *theEvent) HideWindow(theWindow); } break; + + case inGrow: + // Can only resize the steps window at this point + HandleGrowWindow(theWindow, theEvent, windowKind); + break; + + case inZoomIn: + case inZoomOut: + if (TrackBox(theWindow, theEvent->where, windowCode)) + { + HandleZoomWindow(theWindow, theEvent, windowCode, windowKind); + } + break; } } @@ -86,11 +143,13 @@ static void HandleEvent(void) HiliteMenu(0); SystemTask(); /* Handle desk accessories */ + // TODO: Replace SystemTask/GetNextEvent with WaitNextEvent (System 7+ only?) ok = GetNextEvent(everyEvent, &theEvent); if (ok) { - + short windowKind; theWindow = FrontWindow(); + windowKind = ((WindowPeek)theWindow)->windowKind; switch (theEvent.what) { @@ -109,8 +168,29 @@ static void HandleEvent(void) case updateEvt: if (theWindow != NULL) { + RgnHandle oldClipRegion = theWindow->clipRgn; + RgnHandle clipRegion = NewRgn(); + BeginUpdate(theWindow); - // Update windows + + // TODO: I don't think we need to erase/redraw on every update cycle + // but it is the only way I can get it to work for now + EraseRect(&theWindow->portRect); + UpdateControls(theWindow, theWindow->visRgn); + + // Clip out the frame for the bottom scrollbar which we don't use right now + SetRectRgn(clipRegion, theWindow->portRect.right - kScrollbarAdjust, theWindow->portRect.top, theWindow->portRect.right, theWindow->portRect.bottom); + theWindow->clipRgn = clipRegion; + DrawGrowIcon(theWindow); + theWindow->clipRgn = oldClipRegion; + DisposeRgn(clipRegion); + + // Window specific update logic + if (windowKind == kViewSessionWindowId) + { + SessionViewUpdate(theWindow); + } + EndUpdate(theWindow); } diff --git a/macbrew-ui/mbConstants.h b/macbrew-ui/mbConstants.h index ceca067..d8acfbe 100644 --- a/macbrew-ui/mbConstants.h +++ b/macbrew-ui/mbConstants.h @@ -5,6 +5,7 @@ // WIND #define kSplashWindowId 128 #define kViewSessionWindowId 129 +#define kViewStepsWindowId 130 // PICT #define kSplashImageId 128 @@ -48,3 +49,19 @@ // Fermentation Graph #define kGraphWidth 320 #define kGraphHeight 180 + +// Scrollbar Constants +#define kScrollbarWidth 16 // conventional width +#define kScrollbarAdjust (kScrollbarWidth - 1) // to align with window frame +#define kScrollTweek 2 // to align scroll bars with size box + +// Brew Session Steps +#define kBrewSessionStepHeight 20 +#define kBrewSessionWindowHPadding 10 +#define kBrewSessionWindowVPadding 0 +#define kBrewSessionScrollPageOverlap 20 // The amount of overlap of the previous page to show to provide context when paging + +// Window Constants +#define kMinWindowSize 64 +// Docs say 65,535 can be used as max but it seems to overflow the short data type and break the resizing +#define kMaxWindowSize 512 diff --git a/macbrew-ui/mbDSessionList.c b/macbrew-ui/mbDSessionList.c index 9890a96..523fc87 100644 --- a/macbrew-ui/mbDSessionList.c +++ b/macbrew-ui/mbDSessionList.c @@ -10,8 +10,6 @@ typedef struct SessionListDialogState { ListHandle listHandle; - ControlHandle cancelButton; - ControlHandle okButton; ListItem **sessionListItems; short sessionListItemCount; } SessionListDialogState; diff --git a/macbrew-ui/mbDataManager.c b/macbrew-ui/mbDataManager.c index 82ca947..be3ff03 100644 --- a/macbrew-ui/mbDataManager.c +++ b/macbrew-ui/mbDataManager.c @@ -16,11 +16,13 @@ static void InitReader(ResponseReader *reader, const SerialResponse *responseDat static void ReadBool(ResponseReader *reader, Boolean *outBoolean); static void ReadChar(ResponseReader *reader, char *outChar); static void ReadUnsignedChar(ResponseReader *reader, unsigned char *outChar); +static void ReadShort(ResponseReader *reader, short *outShort); static void ReadUnsignedShort(ResponseReader *reader, unsigned short *outShort); static void ReadUnsignedLong(ResponseReader *reader, unsigned long *outUnsignedLong); static void ReadString(ResponseReader *reader, StringHandle *outString); static void ReadSequence(ResponseReader *reader, Sequence *outSequence); static void ReadBrewSessionReference(ResponseReader *reader, Handle *outHandle); +static void ReadBrewSessionStep(ResponseReader *reader, Handle *outHandle); static void ValidateResponse(ResponseReader *reader); static void AssertReaderEnd(const ResponseReader *reader, const SerialResponse *responseData); static char *BuildCommand(const char *formatString, const unsigned char *arg); @@ -75,6 +77,16 @@ static void ReadUnsignedChar(ResponseReader *reader, unsigned char *outChar) HUnlock(reader->response->data); } +static void ReadShort(ResponseReader *reader, short *outShort) +{ + short value; + HLock(reader->response->data); + value = GetShortFromBuffer(*reader->response->data, reader->cursor); + reader->cursor += sizeof(short); + *outShort = value; + HUnlock(reader->response->data); +} + static void ReadUnsignedShort(ResponseReader *reader, unsigned short *outShort) { unsigned short value; @@ -147,6 +159,30 @@ static void ReadBrewSessionReference(ResponseReader *reader, Handle *outHandle) *outHandle = handle; } +static void ReadBrewSessionStep(ResponseReader *reader, Handle *outHandle) +{ + Handle handle = NewHandle(sizeof(BrewSessionStep)); + BrewSessionStep *brewSessionStep; + unsigned char rawPhase; + + HLock(handle); + + // StringHandle description; + // short time; + // BrewSessionStepPhase phase; + + brewSessionStep = (BrewSessionStep *)*handle; + + ReadString(reader, &brewSessionStep->description); + ReadShort(reader, &brewSessionStep->time); + ReadUnsignedChar(reader, &rawPhase); + brewSessionStep->phase = rawPhase; + + HUnlock(handle); + + *outHandle = handle; +} + static void ValidateResponse(ResponseReader *reader) { Boolean success; @@ -185,7 +221,6 @@ void Ping() void FetchBrewSessionReferences(Sequence **outSessionReferences) { - SerialResponse *responseData; ResponseReader reader; Sequence *sessionReference = (Sequence *)NewPtr(sizeof(Sequence)); @@ -322,3 +357,40 @@ void FetchFermentationData(StringHandle sessionId, FermentationDataHandle *outHa *outHandle = (FermentationDataHandle)handle; } + +void FetchBrewSessionSteps(StringHandle sessionId, struct Sequence **outSessionSteps) +{ + Str255 command; + Str255 cSessionId; + SerialResponse *responseData; + ResponseReader reader; + Sequence *sessionSteps = (Sequence *)NewPtr(sizeof(Sequence)); + short i; + + HLock((Handle)sessionId); + PascalToCStringCopy(cSessionId, *sessionId); + HUnlock((Handle)sessionId); + + sprintf((char *)command, "1 LIST STEP %s\r", cSessionId); + + SetUpSerial(); + SendCommand((char *)command); + ReadResponse(&responseData); + TearDownSerial(); + + InitReader(&reader, responseData); + + ValidateResponse(&reader); + + ReadSequence(&reader, sessionSteps); + for (i = 0; i < sessionSteps->size; i++) + { + ReadBrewSessionStep(&reader, &sessionSteps->elements[i]); + } + + AssertReaderEnd(&reader, responseData); + + *outSessionSteps = sessionSteps; + + DisposeResponse(&responseData); +} diff --git a/macbrew-ui/mbDataManager.h b/macbrew-ui/mbDataManager.h index 1794ef0..126b1dd 100644 --- a/macbrew-ui/mbDataManager.h +++ b/macbrew-ui/mbDataManager.h @@ -7,3 +7,4 @@ void Ping(void); void FetchBrewSessionReferences(struct Sequence **outSessionReferences); void FetchBrewSession(StringHandle sessionId, struct BrewSession ***outHandle); void FetchFermentationData(StringHandle sessionId, struct FermentationData ***outHandle); +void FetchBrewSessionSteps(StringHandle sessionId, struct Sequence **outSessionSteps); diff --git a/macbrew-ui/mbDialogUtils.c b/macbrew-ui/mbDialogUtils.c index e4c6dab..b60f111 100644 --- a/macbrew-ui/mbDialogUtils.c +++ b/macbrew-ui/mbDialogUtils.c @@ -1,5 +1,7 @@ #include "mbDialogUtils.h" +#include + pascal void ButtonOutlineDrawProc(DialogPtr theDialog, short theItem); pascal void ButtonOutlineDrawProc(DialogPtr theDialog, short theItem) diff --git a/macbrew-ui/mbMenus.c b/macbrew-ui/mbMenus.c index 6bcc52d..9563c29 100644 --- a/macbrew-ui/mbMenus.c +++ b/macbrew-ui/mbMenus.c @@ -5,13 +5,18 @@ #include "mbDSessionList.h" #include "mbUtil.h" #include "mbWViewSession.h" +#include "mbWViewSteps.h" -MenuHandle appleMenu, fileMenu; +MenuHandle appleMenu, fileMenu, sessionMenu; +// TODO: Support multiple sessions +// TODO: Clear when closing session window +BrewSessionReferenceHandle selectedSession = NULL; enum { appleID = 1, - fileID + fileID, + sessionID }; enum @@ -21,15 +26,25 @@ enum quitItem = 3 }; +enum +{ + stepsItem = 1 +}; + void SetUpMenus(void) { InsertMenu(appleMenu = NewMenu(appleID, "\p\024"), 0); AddResMenu(appleMenu, 'DRVR'); + InsertMenu(fileMenu = NewMenu(fileID, "\pFile"), 0); - DrawMenuBar(); - AppendMenu(fileMenu, "\pPing/S"); + AppendMenu(fileMenu, "\pPing/P"); AppendMenu(fileMenu, "\pOpen Session/O"); AppendMenu(fileMenu, "\pQuit/Q"); + + InsertMenu(sessionMenu = NewMenu(sessionID, "\pSession"), 0); + AppendMenu(sessionMenu, "\pSteps/S"); + + DrawMenuBar(); } void HandleMenu(long mSelect) @@ -61,7 +76,6 @@ void HandleMenu(long mSelect) { WindowPtr sessionListDialog = SessionListDialogSetUp(); short selectedItem; - BrewSessionReferenceHandle selectedSession; FermentationDataHandle fermentationData; BrewSessionHandle brewSession; WindowPtr viewSessionWindow; @@ -92,5 +106,26 @@ void HandleMenu(long mSelect) break; } break; + case sessionID: + if (selectedSession == NULL) + { + // Can't open steps if no session selected + return; + } + switch (menuItem) + { + case stepsItem: + { + WindowPtr viewStepsWindow; + Sequence *sessionStepsHandle; + + viewStepsWindow = StepsViewWindowSetUp(); + + FetchBrewSessionSteps((*selectedSession)->id, &sessionStepsHandle); + StepsViewSetSteps(viewStepsWindow, sessionStepsHandle); + break; + } + break; + } } } diff --git a/macbrew-ui/mbTypes.h b/macbrew-ui/mbTypes.h index b126d55..1aaccc3 100644 --- a/macbrew-ui/mbTypes.h +++ b/macbrew-ui/mbTypes.h @@ -66,6 +66,23 @@ typedef struct Recipe Sequence *yeast; } Recipe; +typedef enum +{ + PreparePhase = 16, + MashPhase = 18, + BoilPhase = 20, + ChillPhase = 22, + CleanupPhase = 24 +} BrewSessionStepPhase; + +typedef struct BrewSessionStep +{ + StringHandle description; + short time; + BrewSessionStepPhase phase; +} BrewSessionStep; + typedef BrewSessionReference **BrewSessionReferenceHandle; typedef BrewSession **BrewSessionHandle; typedef FermentationData **FermentationDataHandle; +typedef BrewSessionStep **BrewSessionStepHandle; diff --git a/macbrew-ui/mbWViewSession.c b/macbrew-ui/mbWViewSession.c index ca6e43d..35c75ba 100644 --- a/macbrew-ui/mbWViewSession.c +++ b/macbrew-ui/mbWViewSession.c @@ -16,6 +16,8 @@ static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow); static void *ViewSessionWindowUnlockState(WindowPtr theWindow); static void DrawRow(ConstStr255Param title, StringHandle value, short rowNum); static void SetupQuickDraw(WindowPtr window); +static void DrawSessionInfo(WindowPtr window, BrewSessionHandle brewSessionHandle); +static void DrawGraph(WindowPtr window, FermentationDataHandle fermentationDataHandle); // TODO: Maybe move to a utils file? static void DrawStringHandle(StringHandle stringHandle) @@ -68,34 +70,10 @@ static void SetupQuickDraw(WindowPtr window) PenNormal(); } -WindowPtr SessionViewWindowSetUp(void) -{ - WindowPtr viewSessionWindow = NULL; - viewSessionWindow = GetNewWindow(kViewSessionWindowId, viewSessionWindow, (WindowPtr)-1L); - - ViewSessionWindowInitState(viewSessionWindow); - - SetPort(viewSessionWindow); - - return viewSessionWindow; -} - -void SessionViewWindowDestroy(WindowPtr window) -{ - if (window != NULL) - { - DisposeWindow(window); - window = NULL; - } -} - -void SessionViewSetSession(WindowPtr window, BrewSessionHandle brewSessionHandle) +static void DrawSessionInfo(WindowPtr window, BrewSessionHandle brewSessionHandle) { - unsigned short rowNum = 1; - ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); BrewSession *brewSession = NULL; - windowState->brewSessionHandle = brewSessionHandle; HLock((Handle)brewSessionHandle); brewSession = *brewSessionHandle; @@ -113,18 +91,14 @@ void SessionViewSetSession(WindowPtr window, BrewSessionHandle brewSessionHandle DrawRow("\pStyle:", brewSession->style_name, rowNum++); HUnlock((Handle)brewSessionHandle); - ViewSessionWindowUnlockState(window); } -void SessionViewSetFermentationData(WindowPtr window, struct FermentationData **fermentationDataHandle) +static void DrawGraph(WindowPtr window, FermentationDataHandle fermentationDataHandle) { - ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); FermentationData *fermentationData = NULL; Rect graphFrame; unsigned short i, pointSize, maxTemp = 0, maxGravity = 0; - windowState->fermentationDataHandle = fermentationDataHandle; - SetupQuickDraw(window); HLock((Handle)fermentationDataHandle); @@ -162,6 +136,9 @@ void SessionViewSetFermentationData(WindowPtr window, struct FermentationData ** HUnlock(dataPointHandle); } + // TODO: Labels for the graph + // TODO: Legend for the graph + // TODO: Extract things into functions for DRY for (i = 0; i < fermentationData->graph->size - 1; i++) { // TODO: These points probably shouldn't be handles @@ -191,5 +168,64 @@ void SessionViewSetFermentationData(WindowPtr window, struct FermentationData ** } HUnlock((Handle)fermentationDataHandle); +} + +WindowPtr SessionViewWindowSetUp(void) +{ + WindowPtr viewSessionWindow = NULL; + viewSessionWindow = GetNewWindow(kViewSessionWindowId, viewSessionWindow, (WindowPtr)-1L); + + ViewSessionWindowInitState(viewSessionWindow); + + SetPort(viewSessionWindow); + + return viewSessionWindow; +} + +void SessionViewWindowDestroy(WindowPtr window) +{ + if (window != NULL) + { + DisposeWindow(window); + window = NULL; + } +} + +void SessionViewSetSession(WindowPtr window, BrewSessionHandle brewSessionHandle) +{ + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + windowState->brewSessionHandle = brewSessionHandle; + + DrawSessionInfo(window, brewSessionHandle); + + ViewSessionWindowUnlockState(window); +} + +void SessionViewSetFermentationData(WindowPtr window, struct FermentationData **fermentationDataHandle) +{ + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + windowState->fermentationDataHandle = fermentationDataHandle; + + DrawGraph(window, fermentationDataHandle); + + ViewSessionWindowUnlockState(window); +} + +void SessionViewUpdate(WindowPtr window) +{ + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + BrewSessionHandle brewSessionHandle = windowState->brewSessionHandle; + FermentationDataHandle fermentationDataHandle = windowState->fermentationDataHandle; + + if (brewSessionHandle != NULL) + { + DrawSessionInfo(window, windowState->brewSessionHandle); + } + + if (fermentationDataHandle != NULL) + { + DrawGraph(window, windowState->fermentationDataHandle); + } + ViewSessionWindowUnlockState(window); } diff --git a/macbrew-ui/mbWViewSession.h b/macbrew-ui/mbWViewSession.h index 5163ce2..5e042ec 100644 --- a/macbrew-ui/mbWViewSession.h +++ b/macbrew-ui/mbWViewSession.h @@ -6,3 +6,4 @@ WindowPtr SessionViewWindowSetUp(void); void SessionViewWindowDestroy(WindowPtr window); void SessionViewSetSession(WindowPtr window, struct BrewSession **brewSessionHandle); void SessionViewSetFermentationData(WindowPtr window, struct FermentationData **fermentationDataHandle); +void SessionViewUpdate(WindowPtr window); diff --git a/macbrew-ui/mbWViewSteps.c b/macbrew-ui/mbWViewSteps.c new file mode 100644 index 0000000..3cd31b4 --- /dev/null +++ b/macbrew-ui/mbWViewSteps.c @@ -0,0 +1,299 @@ +#include + +#include "mbConstants.h" +#include "mbWViewSteps.h" +#include "mbTypes.h" + +typedef struct StepsViewWindowState +{ + Sequence *sessionSteps; + ControlHandle scrollBar; + ControlHandle *checkBoxControls; +} StepsViewWindowState; + +static void StepsViewWindowInitState(WindowPtr theWindow); +static StepsViewWindowState *StepsViewWindowLockState(WindowPtr theWindow); +static void *StepsViewWindowUnlockState(WindowPtr theWindow); +static void SetupScrollbar(WindowPtr theWindow); +static void AdjustScrollbar(WindowPtr theWindow, StepsViewWindowState *windowState); +static Rect CalculateCheckboxRect(WindowPtr window, short index); +static void OffsetControls(WindowPtr window, short offset); +static void ScrollByOffset(WindowPtr window, ControlHandle targetControl, short previousScrollValue, short newScrollValue); +pascal void HandleScrollButtonClickedProc(ControlHandle controlHandle, short part); + +static void StepsViewWindowInitState(WindowPtr theWindow) +{ + Handle viewSessionWindowStateHandle = NewHandleClear(sizeof(StepsViewWindowState)); + SetWRefCon(theWindow, (long)viewSessionWindowStateHandle); + ((WindowPeek)theWindow)->windowKind = kViewStepsWindowId; +} + +static StepsViewWindowState *StepsViewWindowLockState(WindowPtr theWindow) +{ + Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + + HLock(viewSessionWindowStateHandle); + return (StepsViewWindowState *)*viewSessionWindowStateHandle; +} + +static void *StepsViewWindowUnlockState(WindowPtr theWindow) +{ + Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + + HUnlock(viewSessionWindowStateHandle); +} + +static void AdjustScrollbar(WindowPtr theWindow, StepsViewWindowState *windowState) +{ + ControlHandle scrollBar = windowState->scrollBar; + Rect portRect = theWindow->portRect; + short maxHeight, currentValue; + + HideControl(scrollBar); + + MoveControl(scrollBar, portRect.right - kScrollbarAdjust, -1); + SizeControl(scrollBar, kScrollbarWidth, (portRect.bottom - portRect.top) - (kScrollbarAdjust - kScrollTweek)); + + maxHeight = (kBrewSessionWindowVPadding + kBrewSessionStepHeight * windowState->sessionSteps->size); + + SetCtlMax(scrollBar, maxHeight); + currentValue = GetCtlValue(scrollBar); + // TODO: Define clamp function? + if (currentValue < 0) + { + SetCtlValue(scrollBar, 0); + } + else if (currentValue > maxHeight) + { + SetCtlValue(scrollBar, maxHeight); + } + + ShowControl(scrollBar); +} + +static void SetupScrollbar(WindowPtr theWindow) +{ + StepsViewWindowState *windowState = StepsViewWindowLockState(theWindow); + + // TODO: Move to resource? + windowState->scrollBar = NewControl(theWindow, &theWindow->portRect, "\p", FALSE, 0, 0, 0, scrollBarProc, 0); + + AdjustScrollbar(theWindow, windowState); + + StepsViewWindowUnlockState(theWindow); +} + +static Rect CalculateCheckboxRect(WindowPtr window, short index) +{ + Rect r; + r.left = kBrewSessionWindowHPadding; + r.top = kBrewSessionWindowVPadding + index * kBrewSessionStepHeight; + r.right = window->portRect.right - kBrewSessionWindowHPadding; + r.bottom = kBrewSessionStepHeight + index * kBrewSessionStepHeight; + return r; +} + +static void OffsetControls(WindowPtr window, short offset) +{ + StepsViewWindowState *windowState = StepsViewWindowLockState(window); + + if (windowState->checkBoxControls != NULL) + { + short i; + for (i = 0; i < windowState->sessionSteps->size; i++) + { + ControlPtr control; + Rect rect = CalculateCheckboxRect(window, i); + ControlHandle controlHandle = windowState->checkBoxControls[i]; + HLock((Handle)controlHandle); + MoveControl(controlHandle, rect.left, rect.top + offset); + SizeControl(controlHandle, rect.right - rect.left, rect.bottom - rect.top); + HUnlock((Handle)controlHandle); + } + } + + StepsViewWindowUnlockState(window); +} + +static void ScrollByOffset(WindowPtr window, ControlHandle targetControl, short previousScrollValue, short newScrollValue) +{ + short offset = previousScrollValue - newScrollValue; + Rect newScrollRect = window->portRect; + RgnHandle newRgn = NewRgn(); + + newScrollRect.right = newScrollRect.right - kScrollbarWidth; + ScrollRect(&newScrollRect, 0, offset, newRgn); + OffsetControls(window, -newScrollValue); + InvalRgn(newRgn); + + DisposeRgn(newRgn); +} + +pascal void HandleScrollButtonClickedProc(ControlHandle controlHandle, short part) +{ + ControlPtr control; + StepsViewWindowState *windowState; + short scrollDistance = 0, minScrollValue = GetCtlMin(controlHandle), + maxScrollValue = GetCtlMax(controlHandle), oldScrollValue = GetCtlValue(controlHandle), newScrollValue; + WindowPtr parentWindow = (*controlHandle)->contrlOwner; + + windowState = StepsViewWindowLockState(parentWindow); + switch (part) + { + case inUpButton: + case inDownButton: + { + scrollDistance = 2; + break; + } + case inPageUp: + case inPageDown: + { + scrollDistance = parentWindow->portRect.bottom - kBrewSessionScrollPageOverlap; + break; + } + } + + if (part == inUpButton || part == inPageUp) + { + newScrollValue = oldScrollValue - scrollDistance; + } + else + { + newScrollValue = oldScrollValue + scrollDistance; + } + + if (newScrollValue < minScrollValue) + { + newScrollValue = minScrollValue; + } + if (newScrollValue > maxScrollValue) + { + newScrollValue = maxScrollValue; + } + + ScrollByOffset(parentWindow, controlHandle, oldScrollValue, newScrollValue); + SetCtlValue(controlHandle, newScrollValue); + + StepsViewWindowUnlockState(parentWindow); +} + +WindowPtr StepsViewWindowSetUp(void) +{ + WindowPtr viewSessionWindow = NULL; + viewSessionWindow = GetNewWindow(kViewStepsWindowId, viewSessionWindow, (WindowPtr)-1L); + + StepsViewWindowInitState(viewSessionWindow); + + SetPort(viewSessionWindow); + + SetWTitle(viewSessionWindow, "\pBrew Session Steps"); + SetupScrollbar(viewSessionWindow); + + return viewSessionWindow; +} + +void StepsViewWindowDestroy(WindowPtr window) +{ + if (window != NULL) + { + DisposeWindow(window); + window = NULL; + } +} + +void StepsViewSetSteps(WindowPtr window, Sequence *sessionSteps) +{ + unsigned short i; + StepsViewWindowState *windowState = StepsViewWindowLockState(window); + windowState->sessionSteps = sessionSteps; + windowState->checkBoxControls = (ControlHandle *)NewPtr(sizeof(ControlHandle) * sessionSteps->size); + + for (i = 0; i < sessionSteps->size; i++) + { + Rect r = CalculateCheckboxRect(window, i); + BrewSessionStep *step; + Handle stepHandle = sessionSteps->elements[i]; + + HLock(stepHandle); + step = (BrewSessionStep *)*stepHandle; + HLock((Handle)step->description); + + windowState->checkBoxControls[i] = NewControl(window, &r, *(step->description), TRUE, 0, 0, 1, checkBoxProc, i); + + HUnlock((Handle)step->description); + HUnlock(stepHandle); + } + + AdjustScrollbar(window, windowState); + + StepsViewWindowUnlockState(window); +} + +void StepsViewHandleMouseDown(EventRecord *theEvent, WindowPtr window) +{ + ControlHandle targetControl; + Point mouse = theEvent->where; + short part; + StepsViewWindowState *windowState = StepsViewWindowLockState(window); + + GlobalToLocal(&mouse); + part = FindControl(mouse, window, &targetControl); + switch (part) + { + // For some reason the Mac developers didn't call this 'inScroll' + case inThumb: + { + RgnHandle newRgn; + Rect newScrollRect; + short oldScrollValue = GetCtlValue(targetControl); + + part = TrackControl(targetControl, mouse, NULL); + // Check that the mouse is still in the control + if (part == inThumb) + { + short newScrollValue = GetCtlValue(targetControl); + ScrollByOffset(window, targetControl, oldScrollValue, newScrollValue); + } + break; + } + case inCheckBox: + { + part = TrackControl(targetControl, mouse, NULL); + if (part != 0) + { + short currentValue = GetCtlValue(targetControl); + SetCtlValue(targetControl, 1 - currentValue); + // TODO: Perist selected steps? + } + } + case inUpButton: + case inDownButton: + case inPageUp: + case inPageDown: + { + if (targetControl == windowState->scrollBar) + { + part = TrackControl(targetControl, mouse, &HandleScrollButtonClickedProc); + } + break; + } + } + + StepsViewWindowUnlockState(window); +} + +void StepsViewHandleGrow(EventRecord *theEvent, WindowPtr window) +{ + StepsViewWindowState *windowState = StepsViewWindowLockState(window); + + // Redraw the scrollbar in the right place + AdjustScrollbar(window, windowState); + // For the controls to resize + OffsetControls(window, 0); + // Make the window render everything again + // TODO: Partial updates + InvalRect(&window->portRect); + + StepsViewWindowUnlockState(window); +} diff --git a/macbrew-ui/mbWViewSteps.h b/macbrew-ui/mbWViewSteps.h new file mode 100644 index 0000000..7556187 --- /dev/null +++ b/macbrew-ui/mbWViewSteps.h @@ -0,0 +1,9 @@ + +struct BrewSession; +struct FermentationData; + +WindowPtr StepsViewWindowSetUp(void); +void StepsViewWindowDestroy(WindowPtr window); +void StepsViewSetSteps(WindowPtr window, struct Sequence *sessionSteps); +void StepsViewHandleMouseDown(EventRecord *theEvent, WindowPtr window); +void StepsViewHandleGrow(EventRecord *theEvent, WindowPtr window); From 5bf17e7f27634d3b534c0273bd6f621a66c36ed0 Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Thu, 2 Sep 2021 12:34:50 +1000 Subject: [PATCH 08/10] docs: update README --- README.md | 15 +++++++++++++++ docs/session-list.png | Bin 8716 -> 11298 bytes docs/session-view.png | Bin 0 -> 9019 bytes docs/step-list.png | Bin 0 -> 12724 bytes 4 files changed, 15 insertions(+) create mode 100644 docs/session-view.png create mode 100644 docs/step-list.png diff --git a/README.md b/README.md index af8c1fa..221fdf0 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,19 @@ how software was developed 30 years ago. Bonus points if it helps me brew better beer. +## Status + +I'm working my way towards brewing a beer with +just macbrew as an aid. + +Once I have a recipe view I should be able to do that. + +- [x] Load basic Brew Session data +- [x] Generate brewing steps +- [ ] Show recipe + +## Components + ### macbrew-proxy A modern application written in Rust that will run on a Raspberry Pi @@ -63,6 +76,8 @@ Some screenshots of the work in progress: ![Screenshot of the splash screen](./docs/splash.png "Screenshot of the splash screen") ![Screenshot of the session list](./docs/session-list.png "Screenshot of the session list") +![Screenshot of the session view](./docs/session-view.png "Screenshot of the session view") +![Screenshot of the steps list](./docs/step-list.png "Screenshot of the steps list") ## License diff --git a/docs/session-list.png b/docs/session-list.png index 128e6b6ba717f2386a05560204df6a4298246a7a..1cb0cc39fb10aa51c9e96f8f92833112dcda02c5 100644 GIT binary patch literal 11298 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 diff --git a/docs/session-view.png b/docs/session-view.png new file mode 100644 index 0000000000000000000000000000000000000000..d30c6d40fb6d5d219fae7042a3f9f081181f7aa4 GIT binary patch literal 9019 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 Date: Fri, 3 Sep 2021 11:10:26 +1000 Subject: [PATCH 09/10] chore: fix some clippy issues --- .../brewers_friend/bf_api_data_manager.rs | 4 ++-- macbrew-proxy/src/error.rs | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs b/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs index a0c0110..07c06eb 100644 --- a/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs +++ b/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs @@ -75,11 +75,11 @@ impl BfDataManager for BfApiDataManager { let response: BfBrewSessionsFullResponse = serde_json::from_str(&body)?; let session = match response.brewsessions.as_slice() { - [] => Err(Error::ApiResponseValidationError { + [] => Err(Error::ApiResponseValidation { message: String::from("No session with ID found"), }), [session] => Ok(session.to_owned()), - [_args @ ..] => Err(Error::ApiResponseValidationError { + [_args @ ..] => Err(Error::ApiResponseValidation { message: String::from("Multiple sessions for ID found. This should not happen"), }), }; diff --git a/macbrew-proxy/src/error.rs b/macbrew-proxy/src/error.rs index dd854c9..c7b23fd 100644 --- a/macbrew-proxy/src/error.rs +++ b/macbrew-proxy/src/error.rs @@ -10,37 +10,37 @@ pub enum Error { SerializationFailure {}, #[snafu(display("Error calling API: {}", inner_error))] - ApiError { + Api { #[serde(skip_serializing)] inner_error: ReqwestError, }, #[snafu(display("Error derserializing XML: {}", inner_error))] - XmlError { + Xml { #[serde(skip_serializing)] inner_error: DeError, }, #[snafu(display("Error derserializing JSON: {}", inner_error))] - JsonError { + Json { #[serde(skip_serializing)] inner_error: serde_json::Error, }, #[snafu(display("API response failed validation: {}", message))] - ApiResponseValidationError { message: String }, + ApiResponseValidation { message: String }, #[snafu(display("Invalid command input: {}", message))] InvalidCommandInput { message: String }, #[snafu(display("Serial IO error: {}", inner_error))] - SerialIoError { + SerialIo { #[serde(skip_serializing)] inner_error: std::io::Error, }, #[snafu(display("Unknown error: {}", message))] - UnknownError { message: String }, + Unknown { message: String }, #[snafu(display("Could not allocate serialization buffer. (Not enough RAM?)"))] SerializeBufferFull {}, @@ -53,25 +53,25 @@ pub type Result = std::result::Result; impl From for Error { fn from(inner_error: ReqwestError) -> Self { - Error::ApiError { inner_error } + Error::Api { inner_error } } } impl From for Error { fn from(inner_error: std::io::Error) -> Self { - Error::SerialIoError { inner_error } + Error::SerialIo { inner_error } } } impl From for Error { fn from(inner_error: DeError) -> Self { - Error::XmlError { inner_error } + Error::Xml { inner_error } } } impl From for Error { fn from(inner_error: serde_json::Error) -> Self { - Error::JsonError { inner_error } + Error::Json { inner_error } } } @@ -80,7 +80,7 @@ impl ser::Error for Error { fn custom(msg: T) -> Error { // TODO: What should this actually be doing? // Hint: https://github.com/serde-rs/json/blob/master/src/error.rs#L373 - Error::UnknownError { + Error::Unknown { message: msg.to_string(), } } From 3120fb666ae2c48ab59a6a500186e6d0c59073fd Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Fri, 3 Sep 2021 11:10:42 +1000 Subject: [PATCH 10/10] refactor: dispose windows on close - Try and ensure that the correct port is set when drawing controls to avoid weird issues --- macbrew-ui/macbrew.c | 21 +++++++-- macbrew-ui/mbDSessionList.c | 1 - macbrew-ui/mbDataManager.c | 4 -- macbrew-ui/mbMenus.c | 8 ++++ macbrew-ui/mbSerial.c | 2 +- macbrew-ui/mbWViewSession.c | 72 ++++++++++++++++++++++------ macbrew-ui/mbWViewSteps.c | 94 ++++++++++++++++++++++++++++++++----- 7 files changed, 167 insertions(+), 35 deletions(-) diff --git a/macbrew-ui/macbrew.c b/macbrew-ui/macbrew.c index f997de1..d8b9218 100644 --- a/macbrew-ui/macbrew.c +++ b/macbrew-ui/macbrew.c @@ -42,7 +42,7 @@ static void HandleZoomWindow(WindowPtr thisWindow, EventRecord *event, short zoo static void HandleGrowWindow(WindowPtr thisWindow, EventRecord *event, short windowKind) { - Rect oldViewRect = thisWindow->portRect, limitRect; + Rect limitRect; long growSize; SetRect(&limitRect, kMinWindowSize, kMinWindowSize, kMaxWindowSize, kMaxWindowSize); @@ -52,7 +52,6 @@ static void HandleGrowWindow(WindowPtr thisWindow, EventRecord *event, short win if (growSize != 0) { - Rect newViewRect; // Do the actual resize SizeWindow(thisWindow, LoWord(growSize), HiWord(growSize), TRUE); @@ -115,7 +114,18 @@ static void HandleMouseDown(EventRecord *theEvent) if ( TrackGoAway(theWindow, theEvent->where)) { - HideWindow(theWindow); + if (windowKind == kViewSessionWindowId) + { + SessionViewWindowDestroy(theWindow); + } + else if (windowKind == kViewStepsWindowId) + { + StepsViewWindowDestroy(theWindow); + } + else + { + HideWindow(theWindow); + } } break; @@ -139,6 +149,7 @@ static void HandleEvent(void) int ok; EventRecord theEvent; WindowPtr theWindow; + GrafPtr savePort; HiliteMenu(0); SystemTask(); /* Handle desk accessories */ @@ -151,6 +162,9 @@ static void HandleEvent(void) theWindow = FrontWindow(); windowKind = ((WindowPeek)theWindow)->windowKind; + GetPort(&savePort); + SetPort(theWindow); + switch (theEvent.what) { case mouseDown: @@ -203,6 +217,7 @@ static void HandleEvent(void) } break; } + SetPort(savePort); } } diff --git a/macbrew-ui/mbDSessionList.c b/macbrew-ui/mbDSessionList.c index 523fc87..f394419 100644 --- a/macbrew-ui/mbDSessionList.c +++ b/macbrew-ui/mbDSessionList.c @@ -324,7 +324,6 @@ static Boolean SessionListControlMouseDown(DialogPtr theDialog, EventRecord theE Boolean itemSelected = false; const SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); const ListRec *sessionList = SessionListControlLock(dialogState); - ControlHandle selectedControl; SetPort(sessionList->port); GlobalToLocal(&theEvent.where); diff --git a/macbrew-ui/mbDataManager.c b/macbrew-ui/mbDataManager.c index be3ff03..a625d94 100644 --- a/macbrew-ui/mbDataManager.c +++ b/macbrew-ui/mbDataManager.c @@ -167,10 +167,6 @@ static void ReadBrewSessionStep(ResponseReader *reader, Handle *outHandle) HLock(handle); - // StringHandle description; - // short time; - // BrewSessionStepPhase phase; - brewSessionStep = (BrewSessionStep *)*handle; ReadString(reader, &brewSessionStep->description); diff --git a/macbrew-ui/mbMenus.c b/macbrew-ui/mbMenus.c index 9563c29..4b4034c 100644 --- a/macbrew-ui/mbMenus.c +++ b/macbrew-ui/mbMenus.c @@ -99,6 +99,10 @@ void HandleMenu(long mSelect) FetchFermentationData((*selectedSession)->id, &fermentationData); SessionViewSetFermentationData(viewSessionWindow, fermentationData); + + // TODO: We own fermentationData and brewSession so we should probably clean them up + // but we don't know when the window is destroyed so should we get + // transfer ownership to the window and let it clean it up? break; } case quitItem: @@ -123,6 +127,10 @@ void HandleMenu(long mSelect) FetchBrewSessionSteps((*selectedSession)->id, &sessionStepsHandle); StepsViewSetSteps(viewStepsWindow, sessionStepsHandle); + + // TODO: We own sessionStepsHandle so we should probably clean it up + // but we don't know when the window is destroyed so should we get + // transfer ownership to the window and let it clean it up? break; } break; diff --git a/macbrew-ui/mbSerial.c b/macbrew-ui/mbSerial.c index 0c799b5..70c06a9 100644 --- a/macbrew-ui/mbSerial.c +++ b/macbrew-ui/mbSerial.c @@ -12,7 +12,7 @@ // as many bytes as it sends to cancel out the echo // // Note: This needs to be set to zero when emulating in Basilisk II because there is no fake echo there -#define SUPRESS_ECHO 0 +#define SUPRESS_ECHO 1 #define kChecksumBytes 4 // Accounts for \r\n on every response #define kSuffixSize 2 diff --git a/macbrew-ui/mbWViewSession.c b/macbrew-ui/mbWViewSession.c index 35c75ba..e874f7e 100644 --- a/macbrew-ui/mbWViewSession.c +++ b/macbrew-ui/mbWViewSession.c @@ -3,11 +3,13 @@ #include "mbConstants.h" #include "mbWViewSession.h" #include "mbTypes.h" +#include "mbUtil.h" typedef struct ViewSessionWindowState { BrewSessionHandle brewSessionHandle; FermentationDataHandle fermentationDataHandle; + GrafPtr previousPort; } ViewSessionWindowState; static void DrawStringHandle(StringHandle stringHandle); @@ -15,9 +17,10 @@ static void ViewSessionWindowInitState(WindowPtr theWindow); static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow); static void *ViewSessionWindowUnlockState(WindowPtr theWindow); static void DrawRow(ConstStr255Param title, StringHandle value, short rowNum); -static void SetupQuickDraw(WindowPtr window); -static void DrawSessionInfo(WindowPtr window, BrewSessionHandle brewSessionHandle); -static void DrawGraph(WindowPtr window, FermentationDataHandle fermentationDataHandle); +static void SetupQuickDraw(WindowPtr window, ViewSessionWindowState *windowState); +static void CleanupQuickDraw(ViewSessionWindowState *windowState); +static void DrawSessionInfo(WindowPtr window, ViewSessionWindowState *windowState); +static void DrawGraph(WindowPtr window, ViewSessionWindowState *windowState); // TODO: Maybe move to a utils file? static void DrawStringHandle(StringHandle stringHandle) @@ -38,6 +41,11 @@ static ViewSessionWindowState *ViewSessionWindowLockState(WindowPtr theWindow) { Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + if (viewSessionWindowStateHandle == NULL) + { + Panic("\pCannot lock state before calling ViewSessionWindowInitState!"); + } + HLock(viewSessionWindowStateHandle); return (ViewSessionWindowState *)*viewSessionWindowStateHandle; } @@ -61,8 +69,14 @@ static void DrawRow(ConstStr255Param title, StringHandle value, short rowNum) DrawStringHandle(value); } -static void SetupQuickDraw(WindowPtr window) +static void SetupQuickDraw(WindowPtr window, ViewSessionWindowState *windowState) { + if (windowState->previousPort != NULL) + { + Panic("\pSetupQuickDraw called twice without a call to CleanupQuickDraw"); + } + + GetPort(&windowState->previousPort); // Setup QuickDraw SetPort(window); TextFont(geneva); @@ -70,36 +84,48 @@ static void SetupQuickDraw(WindowPtr window) PenNormal(); } -static void DrawSessionInfo(WindowPtr window, BrewSessionHandle brewSessionHandle) +static void CleanupQuickDraw(ViewSessionWindowState *windowState) +{ + // Restore the port that was set before QuickDraw was set up for this window + // This function should always be called after SetupQuickDraw or it will panic + SetPort(windowState->previousPort); + windowState->previousPort = NULL; +} + +static void DrawSessionInfo(WindowPtr window, ViewSessionWindowState *windowState) { unsigned short rowNum = 1; + BrewSessionHandle brewSessionHandle = windowState->brewSessionHandle; BrewSession *brewSession = NULL; HLock((Handle)brewSessionHandle); brewSession = *brewSessionHandle; + SetupQuickDraw(window, windowState); + HLock((Handle)brewSession->recipe_title); // Set title to demonstrate its working for now SetWTitle(window, *(brewSession->recipe_title)); HUnlock((Handle)brewSession->recipe_title); - SetupQuickDraw(window); - DrawRow("\pCreated At:", brewSession->created_at, rowNum++); DrawRow("\pBatch Code:", brewSession->batch_code, rowNum++); DrawRow("\pPhase:", brewSession->phase, rowNum++); DrawRow("\pStyle:", brewSession->style_name, rowNum++); + CleanupQuickDraw(windowState); + HUnlock((Handle)brewSessionHandle); } -static void DrawGraph(WindowPtr window, FermentationDataHandle fermentationDataHandle) +static void DrawGraph(WindowPtr window, ViewSessionWindowState *windowState) { + FermentationDataHandle fermentationDataHandle = windowState->fermentationDataHandle; FermentationData *fermentationData = NULL; Rect graphFrame; unsigned short i, pointSize, maxTemp = 0, maxGravity = 0; - SetupQuickDraw(window); + SetupQuickDraw(window, windowState); HLock((Handle)fermentationDataHandle); fermentationData = *fermentationDataHandle; @@ -136,6 +162,13 @@ static void DrawGraph(WindowPtr window, FermentationDataHandle fermentationDataH HUnlock(dataPointHandle); } + if (maxTemp == 0 || maxGravity == 0) + { + HUnlock((Handle)fermentationDataHandle); + // Protect against divide by zero crashes + return; + } + // TODO: Labels for the graph // TODO: Legend for the graph // TODO: Extract things into functions for DRY @@ -167,6 +200,8 @@ static void DrawGraph(WindowPtr window, FermentationDataHandle fermentationDataH HUnlock(dataPointHandle); } + CleanupQuickDraw(windowState); + HUnlock((Handle)fermentationDataHandle); } @@ -177,7 +212,7 @@ WindowPtr SessionViewWindowSetUp(void) ViewSessionWindowInitState(viewSessionWindow); - SetPort(viewSessionWindow); + // SetPort(viewSessionWindow); return viewSessionWindow; } @@ -186,6 +221,15 @@ void SessionViewWindowDestroy(WindowPtr window) { if (window != NULL) { + // Clean up the state record + Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(window); + if (viewSessionWindowStateHandle != NULL) + { + DisposeHandle(viewSessionWindowStateHandle); + SetWRefCon(window, (long)NULL); + } + + // DisposeWindow automatically cleans up all the controls by calling DisposeControl for us DisposeWindow(window); window = NULL; } @@ -196,7 +240,7 @@ void SessionViewSetSession(WindowPtr window, BrewSessionHandle brewSessionHandle ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); windowState->brewSessionHandle = brewSessionHandle; - DrawSessionInfo(window, brewSessionHandle); + DrawSessionInfo(window, windowState); ViewSessionWindowUnlockState(window); } @@ -206,7 +250,7 @@ void SessionViewSetFermentationData(WindowPtr window, struct FermentationData ** ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); windowState->fermentationDataHandle = fermentationDataHandle; - DrawGraph(window, fermentationDataHandle); + DrawGraph(window, windowState); ViewSessionWindowUnlockState(window); } @@ -219,12 +263,12 @@ void SessionViewUpdate(WindowPtr window) if (brewSessionHandle != NULL) { - DrawSessionInfo(window, windowState->brewSessionHandle); + DrawSessionInfo(window, windowState); } if (fermentationDataHandle != NULL) { - DrawGraph(window, windowState->fermentationDataHandle); + DrawGraph(window, windowState); } ViewSessionWindowUnlockState(window); diff --git a/macbrew-ui/mbWViewSteps.c b/macbrew-ui/mbWViewSteps.c index 3cd31b4..6b183a6 100644 --- a/macbrew-ui/mbWViewSteps.c +++ b/macbrew-ui/mbWViewSteps.c @@ -3,12 +3,14 @@ #include "mbConstants.h" #include "mbWViewSteps.h" #include "mbTypes.h" +#include "mbUtil.h" typedef struct StepsViewWindowState { Sequence *sessionSteps; ControlHandle scrollBar; ControlHandle *checkBoxControls; + GrafPtr previousPort; } StepsViewWindowState; static void StepsViewWindowInitState(WindowPtr theWindow); @@ -19,28 +21,35 @@ static void AdjustScrollbar(WindowPtr theWindow, StepsViewWindowState *windowSta static Rect CalculateCheckboxRect(WindowPtr window, short index); static void OffsetControls(WindowPtr window, short offset); static void ScrollByOffset(WindowPtr window, ControlHandle targetControl, short previousScrollValue, short newScrollValue); +static void SetupQuickDraw(WindowPtr window, StepsViewWindowState *windowState); +static void CleanupQuickDraw(StepsViewWindowState *windowState); pascal void HandleScrollButtonClickedProc(ControlHandle controlHandle, short part); static void StepsViewWindowInitState(WindowPtr theWindow) { - Handle viewSessionWindowStateHandle = NewHandleClear(sizeof(StepsViewWindowState)); - SetWRefCon(theWindow, (long)viewSessionWindowStateHandle); + Handle viewStepsWindowStateHandle = NewHandleClear(sizeof(StepsViewWindowState)); + SetWRefCon(theWindow, (long)viewStepsWindowStateHandle); ((WindowPeek)theWindow)->windowKind = kViewStepsWindowId; } static StepsViewWindowState *StepsViewWindowLockState(WindowPtr theWindow) { - Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + Handle viewStepsWindowStateHandle = (Handle)GetWRefCon(theWindow); - HLock(viewSessionWindowStateHandle); - return (StepsViewWindowState *)*viewSessionWindowStateHandle; + if (viewStepsWindowStateHandle == NULL) + { + Panic("\pCannot lock state before calling StepsViewWindowInitState!"); + } + + HLock(viewStepsWindowStateHandle); + return (StepsViewWindowState *)*viewStepsWindowStateHandle; } static void *StepsViewWindowUnlockState(WindowPtr theWindow) { - Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(theWindow); + Handle viewStepsWindowStateHandle = (Handle)GetWRefCon(theWindow); - HUnlock(viewSessionWindowStateHandle); + HUnlock(viewStepsWindowStateHandle); } static void AdjustScrollbar(WindowPtr theWindow, StepsViewWindowState *windowState) @@ -102,12 +111,17 @@ static void OffsetControls(WindowPtr window, short offset) short i; for (i = 0; i < windowState->sessionSteps->size; i++) { - ControlPtr control; Rect rect = CalculateCheckboxRect(window, i); ControlHandle controlHandle = windowState->checkBoxControls[i]; HLock((Handle)controlHandle); + + HideControl(controlHandle); + MoveControl(controlHandle, rect.left, rect.top + offset); SizeControl(controlHandle, rect.right - rect.left, rect.bottom - rect.top); + + ShowControl(controlHandle); + HUnlock((Handle)controlHandle); } } @@ -129,9 +143,27 @@ static void ScrollByOffset(WindowPtr window, ControlHandle targetControl, short DisposeRgn(newRgn); } +static void SetupQuickDraw(WindowPtr window, StepsViewWindowState *windowState) +{ + if (windowState->previousPort != NULL) + { + Panic("\pSetupQuickDraw called twice without a call to CleanupQuickDraw"); + } + + GetPort(&windowState->previousPort); + SetPort(window); +} + +static void CleanupQuickDraw(StepsViewWindowState *windowState) +{ + // Restore the port that was set before QuickDraw was set up for this window + // This function should always be called after SetupQuickDraw or it will panic + SetPort(windowState->previousPort); + windowState->previousPort = NULL; +} + pascal void HandleScrollButtonClickedProc(ControlHandle controlHandle, short part) { - ControlPtr control; StepsViewWindowState *windowState; short scrollDistance = 0, minScrollValue = GetCtlMin(controlHandle), maxScrollValue = GetCtlMax(controlHandle), oldScrollValue = GetCtlValue(controlHandle), newScrollValue; @@ -181,14 +213,19 @@ pascal void HandleScrollButtonClickedProc(ControlHandle controlHandle, short par WindowPtr StepsViewWindowSetUp(void) { WindowPtr viewSessionWindow = NULL; + StepsViewWindowState *windowState; viewSessionWindow = GetNewWindow(kViewStepsWindowId, viewSessionWindow, (WindowPtr)-1L); StepsViewWindowInitState(viewSessionWindow); - SetPort(viewSessionWindow); + windowState = StepsViewWindowLockState(viewSessionWindow); + SetupQuickDraw(viewSessionWindow, windowState); SetWTitle(viewSessionWindow, "\pBrew Session Steps"); SetupScrollbar(viewSessionWindow); + CleanupQuickDraw(windowState); + + StepsViewWindowUnlockState(viewSessionWindow); return viewSessionWindow; } @@ -197,6 +234,29 @@ void StepsViewWindowDestroy(WindowPtr window) { if (window != NULL) { + StepsViewWindowState *windowState = StepsViewWindowLockState(window); + Handle viewStepsWindowStateHandle = (Handle)GetWRefCon(window); + + // DisposeWindow automatically cleans up all the controls by calling DisposeControl for us + // so we just need to clean up the containers + DisposePtr((Ptr)windowState->checkBoxControls); + + // We don't really need to null out these values but it can help when debugging + windowState->scrollBar = NULL; + windowState->checkBoxControls = NULL; + // Note: Don't destroy the session steps list because the windows doesn't own them (yet) + windowState->sessionSteps = NULL; + + StepsViewWindowUnlockState(window); + + // Clean up the state record + if (viewStepsWindowStateHandle != NULL) + { + DisposeHandle(viewStepsWindowStateHandle); + SetWRefCon(window, (long)NULL); + } + + // DisposeWindow automatically cleans up all the controls by calling DisposeControl for us DisposeWindow(window); window = NULL; } @@ -209,6 +269,8 @@ void StepsViewSetSteps(WindowPtr window, Sequence *sessionSteps) windowState->sessionSteps = sessionSteps; windowState->checkBoxControls = (ControlHandle *)NewPtr(sizeof(ControlHandle) * sessionSteps->size); + SetupQuickDraw(window, windowState); + for (i = 0; i < sessionSteps->size; i++) { Rect r = CalculateCheckboxRect(window, i); @@ -227,6 +289,8 @@ void StepsViewSetSteps(WindowPtr window, Sequence *sessionSteps) AdjustScrollbar(window, windowState); + CleanupQuickDraw(windowState); + StepsViewWindowUnlockState(window); } @@ -237,6 +301,8 @@ void StepsViewHandleMouseDown(EventRecord *theEvent, WindowPtr window) short part; StepsViewWindowState *windowState = StepsViewWindowLockState(window); + SetupQuickDraw(window, windowState); + GlobalToLocal(&mouse); part = FindControl(mouse, window, &targetControl); switch (part) @@ -244,8 +310,6 @@ void StepsViewHandleMouseDown(EventRecord *theEvent, WindowPtr window) // For some reason the Mac developers didn't call this 'inScroll' case inThumb: { - RgnHandle newRgn; - Rect newScrollRect; short oldScrollValue = GetCtlValue(targetControl); part = TrackControl(targetControl, mouse, NULL); @@ -280,6 +344,8 @@ void StepsViewHandleMouseDown(EventRecord *theEvent, WindowPtr window) } } + CleanupQuickDraw(windowState); + StepsViewWindowUnlockState(window); } @@ -287,6 +353,8 @@ void StepsViewHandleGrow(EventRecord *theEvent, WindowPtr window) { StepsViewWindowState *windowState = StepsViewWindowLockState(window); + SetupQuickDraw(window, windowState); + // Redraw the scrollbar in the right place AdjustScrollbar(window, windowState); // For the controls to resize @@ -295,5 +363,7 @@ void StepsViewHandleGrow(EventRecord *theEvent, WindowPtr window) // TODO: Partial updates InvalRect(&window->portRect); + CleanupQuickDraw(windowState); + StepsViewWindowUnlockState(window); }