diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 7326121..6f800fa 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": [ @@ -13,14 +12,15 @@ "${myDefaultIncludePath}" ], "defines": [ - "FOO", - "BAR=100" + "TARGET_OS_MAC", + "OLDROUTINENAMES" ], "forcedInclude": [ - "${workspaceFolder}/include/MacIncludes.h" + "${workspaceFolder}/include/MacIncludes.h", + "${workspaceFolder}/include/CIncludes/Carbon.h" ], "cStandard": "c89" } ], "version": 4 -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index a19600c..fc4fbb1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,6 +26,21 @@ "mbtypes.h": "c", "string": "cpp", "cstring": "c", - "mbdsessionlist.h": "c" - } -} \ No newline at end of file + "mbdsessionlist.h": "c", + "events.h": "c", + "string_view": "c", + "mbwviewsession.h": "c", + "*.tcc": "c", + "cstdio": "c", + "string.h": "c", + "stdio.h": "c", + "textutils.h": "c", + "quickdraw.h": "c", + "mbserial.h": "c", + "controldefinitions.h": "c", + "mbutil.h": "c" + }, + "c-cpp-flylint.clang.enable": false, + "c-cpp-flylint.flexelint.enable": false, + "c-cpp-flylint.lizard.enable": false +} diff --git a/Images/basilisk_ii_prefs b/Images/basilisk_ii_prefs index f03fa6f..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/1 +seriala /dev/pts/4 serialb /dev/ttyS1 udptunnel false udpport 6066 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 128e6b6..1cb0cc3 100644 Binary files a/docs/session-list.png and b/docs/session-list.png differ diff --git a/docs/session-view.png b/docs/session-view.png new file mode 100644 index 0000000..d30c6d4 Binary files /dev/null and b/docs/session-view.png differ diff --git a/docs/step-list.png b/docs/step-list.png new file mode 100644 index 0000000..33da011 Binary files /dev/null and b/docs/step-list.png differ 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__ 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/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/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/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 952501a..a126591 100644 --- a/macbrew-proxy/src/commands/mod.rs +++ b/macbrew-proxy/src/commands/mod.rs @@ -1,5 +1,7 @@ pub mod command; +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/bf_api_data_manager.rs b/macbrew-proxy/src/data/brewers_friend/bf_api_data_manager.rs index cbc5bc6..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 @@ -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,14 +72,14 @@ 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 { + [] => 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"), }), }; @@ -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/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/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..d94d8fe 100644 --- a/macbrew-proxy/src/data/macbrew/mod.rs +++ b/macbrew-proxy/src/data/macbrew/mod.rs @@ -1,2 +1,5 @@ +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/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/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/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(), } } diff --git a/macbrew-proxy/src/main.rs b/macbrew-proxy/src/main.rs index 02f6e64..e1ed05c 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 { @@ -51,6 +51,16 @@ async fn handle_line(line: &str) -> Result> { ) .await } + ["GET", "FERMENTATION", args @ ..] => { + commands::get_fermentation::GetFermentationDataCommand::::handle( + request_id, args, + ) + .await + } + ["LIST", "STEP", args @ ..] => { + commands::list_steps::ListStepsCommand::::handle(request_id, args) + .await + } ["GET", "RECIPE", args @ ..] => { commands::get_recipes::GetRecipesCommand::::handle( request_id, args, @@ -221,6 +231,69 @@ 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_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_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_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 da042de..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,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 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 54 6F 79 4A | .•1072961ToyJ |", + "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 17fccdf..990e085 100644 Binary files a/macbrew-ui/MacBrewUI.bin and b/macbrew-ui/MacBrewUI.bin differ diff --git a/macbrew-ui/MacBrewUI.rsrc.bin b/macbrew-ui/MacBrewUI.rsrc.bin index 67a0dc4..bfdcca4 100644 Binary files a/macbrew-ui/MacBrewUI.rsrc.bin and b/macbrew-ui/MacBrewUI.rsrc.bin differ diff --git a/macbrew-ui/macbrew.c b/macbrew-ui/macbrew.c index f01a55e..d8b9218 100644 --- a/macbrew-ui/macbrew.c +++ b/macbrew-ui/macbrew.c @@ -5,7 +5,12 @@ #include "mbDSessionList.h" #include "mbSerial.h" +#include "mbWViewSession.h" +#include "mbWViewSteps.h" + static void InitMacintosh(void); +static void HandleZoomWindow(WindowPtr thisWindow, EventRecord *event, short zoomInOrOut, short windowKind); +static void HandleGrowWindow(WindowPtr thisWindow, EventRecord *event, short windowKind); static void HandleMouseDown(EventRecord *theEvent); static void HandleEvent(void); @@ -23,6 +28,44 @@ static void InitMacintosh(void) InitCursor(); } +static void HandleZoomWindow(WindowPtr thisWindow, EventRecord *event, short zoomInOrOut, short windowKind) +{ + // TODO: Add all that boring logic to handle multiple screens + ZoomWindow(thisWindow, zoomInOrOut, thisWindow == FrontWindow()); + + // Invalidate window + if (windowKind == kViewStepsWindowId) + { + StepsViewHandleGrow(event, thisWindow); + } +} + +static void HandleGrowWindow(WindowPtr thisWindow, EventRecord *event, short windowKind) +{ + Rect limitRect; + long growSize; + + SetRect(&limitRect, kMinWindowSize, kMinWindowSize, kMaxWindowSize, kMaxWindowSize); + + // Track the grow button action + growSize = GrowWindow(thisWindow, event->where, &limitRect); + + if (growSize != 0) + { + // 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; @@ -45,38 +88,57 @@ 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()) { 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: if ( TrackGoAway(theWindow, theEvent->where)) { - HideWindow(theWindow); - if (windowKind == kSessionListWindowId) + if (windowKind == kViewSessionWindowId) + { + SessionViewWindowDestroy(theWindow); + } + else if (windowKind == kViewStepsWindowId) { - SessionListDialogDestroy(theWindow); + StepsViewWindowDestroy(theWindow); } + else + { + 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; } @@ -87,15 +149,21 @@ static void HandleEvent(void) int ok; EventRecord theEvent; WindowPtr theWindow; + GrafPtr savePort; 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; + + GetPort(&savePort); + SetPort(theWindow); switch (theEvent.what) { @@ -114,8 +182,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); } @@ -128,6 +217,7 @@ static void HandleEvent(void) } break; } + SetPort(savePort); } } diff --git a/macbrew-ui/mbConstants.h b/macbrew-ui/mbConstants.h index 9dd74af..d8acfbe 100644 --- a/macbrew-ui/mbConstants.h +++ b/macbrew-ui/mbConstants.h @@ -4,7 +4,8 @@ // WIND #define kSplashWindowId 128 -#define kSessionListWindowId 129 +#define kViewSessionWindowId 129 +#define kViewStepsWindowId 130 // PICT #define kSplashImageId 128 @@ -43,4 +44,24 @@ // TODO: Should be defined in MacWindows.h #define kDialogWindowKind 2 -#define kSessionListUserItem 3 \ No newline at end of file +#define kSessionListUserItem 3 + +// 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 1e56bc3..f394419 100644 --- a/macbrew-ui/mbDSessionList.c +++ b/macbrew-ui/mbDSessionList.c @@ -1,3 +1,4 @@ +#include #include #include "mbConstants.h" #include "mbUtil.h" @@ -9,8 +10,8 @@ typedef struct SessionListDialogState { ListHandle listHandle; - ControlHandle cancelButton; - ControlHandle okButton; + ListItem **sessionListItems; + short sessionListItemCount; } SessionListDialogState; pascal Boolean SessionListEventFilterProc(DialogPtr theDialog, EventRecord *theEvent, short *itemHit); @@ -23,9 +24,10 @@ static void SetUpSessionListControl(DialogPtr parentDialog); static void DestroySessionListControl(DialogPtr parentDialog); static ListRec *SessionListControlLock(const SessionListDialogState *dialogState); static void SessionListControlUnlock(const SessionListDialogState *dialogState); +Cell SessionListGetSelectedCell(DialogPtr theDialog); static void SessionListControlHandleKeyboard(DialogPtr theDialog, char key); static void SessionListControlUpdate(DialogPtr theDialog); -static void SessionListControlMouseDown(DialogPtr theDialog, EventRecord theEvent); +static Boolean SessionListControlMouseDown(DialogPtr theDialog, EventRecord theEvent); static void SetUpButtons(DialogPtr parentDialog); static void DestroyButtons(DialogPtr parentDialog); @@ -49,9 +51,10 @@ pascal Boolean SessionListEventFilterProc(DialogPtr theDialog, EventRecord *theE { windowKind = ((WindowPeek)theWindow)->windowKind; } - if (windowCode == inContent && windowKind == kDialogWindowKind) + if (windowCode == inContent && windowKind == kDialogWindowKind && SessionListControlMouseDown(theDialog, *theEvent)) { - SessionListControlMouseDown(theDialog, *theEvent); + *itemHit = ok; + return TRUE; } } else @@ -116,14 +119,23 @@ 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); } 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); @@ -193,6 +205,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,36 +229,39 @@ void SessionListDialogDestroy(DialogPtr theDialog) void SessionListDialogSetSessions(DialogPtr theDialog, Sequence *sessionReferences) { short i; - Cell cell = {0}; - const SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); - const ListRec *sessionList = SessionListControlLock(dialogState); + SessionListDialogState *dialogState = SessionListDialogLockState(theDialog); - 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"); } + 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); SessionListDialogUnlockState(theDialog); } -void SessionListDialogShow(DialogPtr theDialog) +short SessionListDialogShow(DialogPtr theDialog) { short itemHit; ShowWindow(theDialog); @@ -245,13 +271,22 @@ void SessionListDialogShow(DialogPtr theDialog) ModalDialog(&SessionListEventFilterProc, &itemHit); } while (itemHit != ok && itemHit != cancel); - SessionListDialogDestroy(theDialog); + 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) { @@ -284,36 +319,22 @@ 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 55b3632..a625d94 100644 --- a/macbrew-ui/mbDataManager.c +++ b/macbrew-ui/mbDataManager.c @@ -1,3 +1,4 @@ +#include #include #include "mbTypes.h" @@ -13,11 +14,18 @@ typedef struct ResponseReader static void InitReader(ResponseReader *reader, const SerialResponse *responseData); 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 ReadString(ResponseReader *reader, unsigned char *outString); +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); // Response Structure // ----------------- @@ -41,34 +49,87 @@ static void InitReader(ResponseReader *reader, const SerialResponse *responseDat static void ReadBool(ResponseReader *reader, Boolean *outBoolean) { - char value = GetCharFromBuffer(*reader->response->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 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 = 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 ReadString(ResponseReader *reader, unsigned char *outString) +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"); } - 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; + + HUnlock(reader->response->data); } static void ReadSequence(ResponseReader *reader, Sequence *outSequence) @@ -82,13 +143,38 @@ 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; - ReadString(reader, brewSessionReference->id); - ReadString(reader, brewSessionReference->batch_code); - ReadString(reader, brewSessionReference->name); + HLock(handle); + + brewSessionReference = (BrewSessionReference *)*handle; + + ReadString(reader, &brewSessionReference->id); + ReadString(reader, &brewSessionReference->batch_code); + ReadString(reader, &brewSessionReference->name); + + HUnlock(handle); + + *outHandle = handle; +} + +static void ReadBrewSessionStep(ResponseReader *reader, Handle *outHandle) +{ + Handle handle = NewHandle(sizeof(BrewSessionStep)); + BrewSessionStep *brewSessionStep; + unsigned char rawPhase; + + HLock(handle); + + brewSessionStep = (BrewSessionStep *)*handle; + + ReadString(reader, &brewSessionStep->description); + ReadShort(reader, &brewSessionStep->time); + ReadUnsignedChar(reader, &rawPhase); + brewSessionStep->phase = rawPhase; + + HUnlock(handle); *outHandle = handle; } @@ -96,8 +182,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 @@ -108,6 +194,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; @@ -122,7 +217,6 @@ void Ping() void FetchBrewSessionReferences(Sequence **outSessionReferences) { - SerialResponse *responseData; ResponseReader reader; Sequence *sessionReference = (Sequence *)NewPtr(sizeof(Sequence)); @@ -143,9 +237,156 @@ void FetchBrewSessionReferences(Sequence **outSessionReferences) ReadBrewSessionReference(&reader, &sessionReference->elements[i]); } + AssertReaderEnd(&reader, responseData); + *outSessionReferences = sessionReference; - // TODO: Disposing! + DisposeResponse(&responseData); +} + +void FetchBrewSession(StringHandle sessionId, BrewSessionHandle *outHandle) +{ + Str255 command; + Str255 cSessionId; + SerialResponse *responseData; + ResponseReader reader; + Handle handle = NewHandle(sizeof(BrewSession)); + BrewSession *brewSession; + + HLock(handle); + + brewSession = (BrewSession *)*handle; + + 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); + ReadString(&reader, &brewSession->style_name); + ReadString(&reader, &brewSession->created_at); + + AssertReaderEnd(&reader, responseData); + + DisposeResponse(&responseData); + + HUnlock(handle); - //DisposeResponse(&responseData); + *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; +} + +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 d40ddbc..126b1dd 100644 --- a/macbrew-ui/mbDataManager.h +++ b/macbrew-ui/mbDataManager.h @@ -1,5 +1,10 @@ struct Sequence; struct SerialResponse; +struct BrewSession; +struct FermentationData; 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/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..4b4034c 100644 --- a/macbrew-ui/mbMenus.c +++ b/macbrew-ui/mbMenus.c @@ -4,13 +4,19 @@ #include "mbTypes.h" #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 @@ -20,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) @@ -59,12 +75,34 @@ void HandleMenu(long mSelect) case listSessionsItem: { WindowPtr sessionListDialog = SessionListDialogSetUp(); + short selectedItem; + FermentationDataHandle fermentationData; + BrewSessionHandle brewSession; + 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]; + HLock((Handle)selectedSession); + FetchBrewSession((*selectedSession)->id, &brewSession); + HUnlock((Handle)selectedSession); + viewSessionWindow = SessionViewWindowSetUp(); + SessionViewSetSession(viewSessionWindow, brewSession); + + 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: @@ -72,5 +110,30 @@ 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); + + // 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/mbTypes.h b/macbrew-ui/mbTypes.h index 952a871..1aaccc3 100644 --- a/macbrew-ui/mbTypes.h +++ b/macbrew-ui/mbTypes.h @@ -1,12 +1,88 @@ -typedef struct BrewSessionReference -{ - Str255 id; - Str255 batch_code; - Str255 name; -} BrewSessionReference; +typedef unsigned long MacEpochTime; typedef struct Sequence { unsigned short size; Handle *elements; } Sequence; + +typedef struct BrewSessionReference +{ + StringHandle id; + StringHandle batch_code; + StringHandle name; +} BrewSessionReference; + +typedef struct BrewSession +{ + StringHandle id; + StringHandle phase; + 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; + 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 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/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/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); diff --git a/macbrew-ui/mbWViewSession.c b/macbrew-ui/mbWViewSession.c new file mode 100644 index 0000000..e874f7e --- /dev/null +++ b/macbrew-ui/mbWViewSession.c @@ -0,0 +1,275 @@ +#include + +#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); +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, 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) +{ + HLock((Handle)stringHandle); + DrawString(*stringHandle); + HUnlock((Handle)stringHandle); +} + +static void ViewSessionWindowInitState(WindowPtr theWindow) +{ + Handle viewSessionWindowStateHandle = NewHandleClear(sizeof(ViewSessionWindowState)); + SetWRefCon(theWindow, (long)viewSessionWindowStateHandle); + ((WindowPeek)theWindow)->windowKind = kViewSessionWindowId; +} + +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; +} + +static void *ViewSessionWindowUnlockState(WindowPtr theWindow) +{ + Handle viewSessionWindowStateHandle = (Handle)GetWRefCon(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, ViewSessionWindowState *windowState) +{ + if (windowState->previousPort != NULL) + { + Panic("\pSetupQuickDraw called twice without a call to CleanupQuickDraw"); + } + + GetPort(&windowState->previousPort); + // Setup QuickDraw + SetPort(window); + TextFont(geneva); + TextSize(12); + PenNormal(); +} + +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); + + 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, ViewSessionWindowState *windowState) +{ + FermentationDataHandle fermentationDataHandle = windowState->fermentationDataHandle; + FermentationData *fermentationData = NULL; + Rect graphFrame; + unsigned short i, pointSize, maxTemp = 0, maxGravity = 0; + + SetupQuickDraw(window, windowState); + + 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); + } + + 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 + 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); + } + + CleanupQuickDraw(windowState); + + 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) + { + // 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; + } +} + +void SessionViewSetSession(WindowPtr window, BrewSessionHandle brewSessionHandle) +{ + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + windowState->brewSessionHandle = brewSessionHandle; + + DrawSessionInfo(window, windowState); + + ViewSessionWindowUnlockState(window); +} + +void SessionViewSetFermentationData(WindowPtr window, struct FermentationData **fermentationDataHandle) +{ + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + windowState->fermentationDataHandle = fermentationDataHandle; + + DrawGraph(window, windowState); + + ViewSessionWindowUnlockState(window); +} + +void SessionViewUpdate(WindowPtr window) +{ + ViewSessionWindowState *windowState = ViewSessionWindowLockState(window); + BrewSessionHandle brewSessionHandle = windowState->brewSessionHandle; + FermentationDataHandle fermentationDataHandle = windowState->fermentationDataHandle; + + if (brewSessionHandle != NULL) + { + DrawSessionInfo(window, windowState); + } + + if (fermentationDataHandle != NULL) + { + DrawGraph(window, windowState); + } + + ViewSessionWindowUnlockState(window); +} diff --git a/macbrew-ui/mbWViewSession.h b/macbrew-ui/mbWViewSession.h new file mode 100644 index 0000000..5e042ec --- /dev/null +++ b/macbrew-ui/mbWViewSession.h @@ -0,0 +1,9 @@ + +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); +void SessionViewUpdate(WindowPtr window); diff --git a/macbrew-ui/mbWViewSteps.c b/macbrew-ui/mbWViewSteps.c new file mode 100644 index 0000000..6b183a6 --- /dev/null +++ b/macbrew-ui/mbWViewSteps.c @@ -0,0 +1,369 @@ +#include + +#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); +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); +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 viewStepsWindowStateHandle = NewHandleClear(sizeof(StepsViewWindowState)); + SetWRefCon(theWindow, (long)viewStepsWindowStateHandle); + ((WindowPeek)theWindow)->windowKind = kViewStepsWindowId; +} + +static StepsViewWindowState *StepsViewWindowLockState(WindowPtr theWindow) +{ + Handle viewStepsWindowStateHandle = (Handle)GetWRefCon(theWindow); + + if (viewStepsWindowStateHandle == NULL) + { + Panic("\pCannot lock state before calling StepsViewWindowInitState!"); + } + + HLock(viewStepsWindowStateHandle); + return (StepsViewWindowState *)*viewStepsWindowStateHandle; +} + +static void *StepsViewWindowUnlockState(WindowPtr theWindow) +{ + Handle viewStepsWindowStateHandle = (Handle)GetWRefCon(theWindow); + + HUnlock(viewStepsWindowStateHandle); +} + +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++) + { + 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); + } + } + + 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); +} + +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) +{ + 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; + StepsViewWindowState *windowState; + viewSessionWindow = GetNewWindow(kViewStepsWindowId, viewSessionWindow, (WindowPtr)-1L); + + StepsViewWindowInitState(viewSessionWindow); + + windowState = StepsViewWindowLockState(viewSessionWindow); + + SetupQuickDraw(viewSessionWindow, windowState); + SetWTitle(viewSessionWindow, "\pBrew Session Steps"); + SetupScrollbar(viewSessionWindow); + CleanupQuickDraw(windowState); + + StepsViewWindowUnlockState(viewSessionWindow); + + return viewSessionWindow; +} + +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; + } +} + +void StepsViewSetSteps(WindowPtr window, Sequence *sessionSteps) +{ + unsigned short i; + StepsViewWindowState *windowState = StepsViewWindowLockState(window); + 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); + 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); + + CleanupQuickDraw(windowState); + + StepsViewWindowUnlockState(window); +} + +void StepsViewHandleMouseDown(EventRecord *theEvent, WindowPtr window) +{ + ControlHandle targetControl; + Point mouse = theEvent->where; + short part; + StepsViewWindowState *windowState = StepsViewWindowLockState(window); + + SetupQuickDraw(window, windowState); + + GlobalToLocal(&mouse); + part = FindControl(mouse, window, &targetControl); + switch (part) + { + // For some reason the Mac developers didn't call this 'inScroll' + case inThumb: + { + 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; + } + } + + CleanupQuickDraw(windowState); + + StepsViewWindowUnlockState(window); +} + +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 + OffsetControls(window, 0); + // Make the window render everything again + // TODO: Partial updates + InvalRect(&window->portRect); + + CleanupQuickDraw(windowState); + + 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);