Monitoring/controlling with Linux on the cheap

Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum

Help Support Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.
OWFS is neat, and powerful if you know what you are doing. But for basic logging, I think most people are going to want to try something canned and ready to go. I liked the LogTemp software I started with for logging. it's point and click and easy to use.

Now you can't do as much with it, but it's basic and easy enough for someone who has not done much programming. If you are familiar with basic programming skills and understand a scripting language, then OWFS is a better choice.

I'll look at both. I think an advantage of owfs is that if the fuse setup doesn't get messed up (too many fuse filesystems I've seen have been lackluster) then privs aren't needed - no setup required in the devfs configuration, but for most people doing brewing multi-user permission schemes and protections don't really provide anything useful.

I do like the fact that the library/daemon under owfs has various language bindings. I may play with this.
 
and I just placed another order. What are you folks clicking on?

EDIT: wait a sec... Yuri. What's the name of your brewery? Kampferstrahl? Where is it located? There's your "business" address. :)

Sorry I meant to say business or education e-mail address, they won't take yahoo or hotmail accounts.
 
Sorry I meant to say business or education e-mail address, they won't take yahoo or hotmail accounts.

Oh, that. Yea, more and more places have that same policy. Can't say I blame 'em. Use the email addy from your ISP.
 
Oh, that. Yea, more and more places have that same policy. Can't say I blame 'em. Use the email addy from your ISP.

lol I didn't even think about that one :) I'll have to find out what it is I've never bothered with it.
 
I just checked, Rogers is now Rogers/Yahoo, they give you a Yahoo account. I'll have to figure out how to setup an e-mail server here and dummy it I guess, if that'll work. Worst case I have to buy a domain name.
 
This to create an account, right? Do they allow Gmail? Do you have a work email you can use long enough to create the account?
 
I'm a stay at home dad, wife's the bread winner. I don't know if they accept gmail, I can try.
 
I just checked, Rogers is now Rogers/Yahoo, they give you a Yahoo account. I'll have to figure out how to setup an e-mail server here and dummy it I guess, if that'll work. Worst case I have to buy a domain name.

Setup your domain in Google Apps (personal edition). Let G handle the email - it's free and far better than any other solution I've ever seen. Google's spam filtering blows the doors of everyone else. Barracuda? A good door stop compared to Google...

http://www.google.com/apps/intl/en/group/index.html
 
Nope Gmail doesn't work. I'll just have to order some, no freebies for me.
 
Here's a code snippet of my page, updated to use the fancy new widgets.

Code:
  // this function calls the Ajax update and draws the canvas elements
  // it is to be executed repeatedly through the use of the setInterval method
  function update() {

    // update data from XML file
    ajaxUpdateValues();

    // draw gauges
    steamPressGauge.setValueAnimated(steamPress);
    steamTempGauge.setValueAnimated(steamTemp);
    mashVolGauge.setValueAnimated(mashVol);
    boilVolGauge.setValueAnimated(boilVol);
    mashTempGauge.setValueAnimated(mashTemp);
    spargeTempGauge.setValueAnimated(spargeTemp);
    boilTempGauge.setValueAnimated(boilTemp);

  }

  // make the script execute at a set interval (in milliseconds)
  function init() {

    // Define section(s)
    var sections = Array(steelseries.Section(10, 12, "rgba(0, 220, 0, 0.3)"),
                         steelseries.Section(12, 15, "rgba(220, 220, 0, 0.3)"),
                         steelseries.Section(15, 20, "rgba(220, 0, 0, 0.3)"));
	
    // Define area(s)
    var areas = Array(steelseries.Section(15, 20, "rgba(220, 0, 0, 0.3)"));
 
    // Initialize gauge		
    steamPressGauge = new steelseries.Radial("steamPress", {
                                             gaugeType: steelseries.GaugeType.TYPE4,
                                             minValue: 0,
                                             maxValue: 20,
                                             threshold: 12,
                                             section: sections,
                                             area: areas,
                                             titleString: "Steam Pressure",
                                             unitString: "psi",
                                             pointerType: steelseries.PointerType.TYPE1,
                                             frameDesign: steelseries.FrameDesign.BLACK_METAL,
                                             foregroundType: steelseries.ForegroundType.TYPE3,
                                             backgroundColor: steelseries.BackgroundColor.CARBON,
                                             ledVisible: false
                                             });

    // Define section(s)
    sections = Array(steelseries.Section(212, 245, "rgba(0, 220, 0, 0.3)"),
                     steelseries.Section(245, 260, "rgba(220, 0, 0, 0.3)"));
	
    // Define area(s)
    areas = Array(steelseries.Section(245, 260, "rgba(220, 0, 0, 0.3)"));

    // Initialize gauge		
    steamTempGauge = new steelseries.Radial("steamTemp", {
                                             gaugeType: steelseries.GaugeType.TYPE3,
                                             minValue: 100,
                                             maxValue: 260,
                                             threshold: 240,
                                             section: sections,
                                             area: areas,
                                             titleString: "Steam Temp",
                                             unitString: "°F",
                                             pointerType: steelseries.PointerType.TYPE1,
                                             frameDesign: steelseries.FrameDesign.BLACK_METAL,
                                             foregroundType: steelseries.ForegroundType.TYPE3,
                                             backgroundColor: steelseries.BackgroundColor.CARBON,
                                             ledVisible: false
                                             });

    // Initialize gauge		
    mashVolGauge = new steelseries.Radial("mashVol", {
                                           gaugeType: steelseries.GaugeType.TYPE4,
                                           minValue: 0,
                                           maxValue: 24,
                                           threshold: 15,
                                           //section: sections,
                                           //area: areas,
                                           titleString: "Mash Volume",
                                           unitString: "Gal",
                                           pointerType: steelseries.PointerType.TYPE1,
                                           frameDesign: steelseries.FrameDesign.BLACK_METAL,
                                           foregroundType: steelseries.ForegroundType.TYPE3,
                                           backgroundColor: steelseries.BackgroundColor.CARBON,
                                           ledVisible: false
                                           });

    // Initialize gauge		
    boilVolGauge = new steelseries.Radial("boilVol", {
                                           gaugeType: steelseries.GaugeType.TYPE4,
                                           minValue: 0,
                                           maxValue: 24,
                                           threshold: 15,
                                           //section: sections,
                                           //area: areas,
                                           titleString: "Boil Volume",
                                           unitString: "Gal",
                                           pointerType: steelseries.PointerType.TYPE1,
                                           frameDesign: steelseries.FrameDesign.BLACK_METAL,
                                           foregroundType: steelseries.ForegroundType.TYPE3,
                                           backgroundColor: steelseries.BackgroundColor.CARBON,
                                           ledVisible: false
                                           });

    // Define section(s)
    sections = Array(steelseries.Section(140, 160, "rgba(0, 220, 0, 0.3)"),
                     steelseries.Section(160, 180, "rgba(220, 220, 0, 0.3)"),
                     steelseries.Section(180, 190, "rgba(220, 0, 0, 0.3)"));
	
    // Define area(s)
    areas = Array(steelseries.Section(180, 190, "rgba(220, 0, 0, 0.3)"));

    // Initialize gauge		
    mashTempGauge = new steelseries.Radial("mashTemp", {
                                            gaugeType: steelseries.GaugeType.TYPE3,
                                            minValue: 100,
                                            maxValue: 190,
                                            threshold: 152,
                                            section: sections,
                                            area: areas,
                                            titleString: "Mash Temp",
                                            unitString: "°F",
                                            pointerType: steelseries.PointerType.TYPE1,
                                            frameDesign: steelseries.FrameDesign.BLACK_METAL,
                                            foregroundType: steelseries.ForegroundType.TYPE3,
                                            backgroundColor: steelseries.BackgroundColor.CARBON,
                                            ledVisible: false
                                            });

    // Define section(s)
    sections = Array(steelseries.Section(160, 180, "rgba(0, 220, 0, 0.3)"),
                     steelseries.Section(180, 190, "rgba(220, 0, 0, 0.3)"));
	
    // Define area(s)
    areas = Array(steelseries.Section(180, 190, "rgba(220, 0, 0, 0.3)"));

    // Initialize gauge		
    spargeTempGauge = new steelseries.Radial("spargeTemp", {
                                            gaugeType: steelseries.GaugeType.TYPE3,
                                            minValue: 100,
                                            maxValue: 190,
                                            threshold: 175,
                                            section: sections,
                                            area: areas,
                                            titleString: "Sparge Temp",
                                            unitString: "°F",
                                            pointerType: steelseries.PointerType.TYPE1,
                                            frameDesign: steelseries.FrameDesign.BLACK_METAL,
                                            foregroundType: steelseries.ForegroundType.TYPE3,
                                            backgroundColor: steelseries.BackgroundColor.CARBON,
                                            ledVisible: false
                                            });

    // Define area(s)
    areas = Array(steelseries.Section(210, 214, "rgba(0, 220, 0, 0.3)"));

    // Initialize gauge		
    boilTempGauge = new steelseries.Radial("boilTemp", {
                                            gaugeType: steelseries.GaugeType.TYPE3,
                                            minValue: 100,
                                            maxValue: 220,
                                            threshold: 212,
                                            //section: sections,
                                            area: areas,
                                            titleString: "Boil Temp",
                                            unitString: "°F",
                                            pointerType: steelseries.PointerType.TYPE1,
                                            frameDesign: steelseries.FrameDesign.BLACK_METAL,
                                            foregroundType: steelseries.ForegroundType.TYPE3,
                                            backgroundColor: steelseries.BackgroundColor.CARBON,
                                            ledVisible: false
                                            });

    setInterval("update();", 1000);
    //setInterval("updateImage();", 3000);
  }
    </script>
 
I couldn't post the whole file because the constructors are rather longwinded, so it exceeds the 10,000 character post limit. The above snippet shows the bulk of the changes to my page. I added a global variable for each instance of a steelseries.Radial object, modified the update() function to work with the new widgets, and added the steelseries.Radial constructors to the init() function. The Ajax code remained the unchanged.

I really like these widgets. The setValueAnimated method will have a debounce effect if there is any sensor "jitter." The gauges paint and update faster than my old BCS-based gauges, even though they are more complex.

I can't control the LEDs on the gauge faces. I think there's an error with the blink logic in the SteelSeries code. I don't feel like dissecting it right now. I've tried setting ledBlink = false. That doesn't work. The LEDs blink at about 1 Hz no matter what, so I set ledVisible = false for now. I think the original intent was for the LED to be on when the gauge value is above the threshold value and off otherwise, but that doesn't seem to be happening.

Also, I'd like to be able to hide the "threshold" marker, but that doesn't seem possible without changing the SteelSeries code. That's probably an easier fix than the LED issue, but I really hate reverse engineering someone else's sparsely commented code.

newgauges.png
 
Ok, one minor change to the Ajax stuff:

Code:
    var now = new Date(); // append the date to GET to avoid caching
    httpRequest.open("GET","xml/brewhut.xml"+ "?" + now.getTime(),false);

Chrome was caching the xml document on an inconsistent basis, sometimes preventing the data from being updated.
 
Yuri,

That looks REALLY nice! I had the same experience with LEDs and the threshold - after a few mins of fooling around I decided I didn't care enough (yet).

RE the 10,000 limit... Have you considered setting up a sourceforge or github for code you're sharing? github is freaking awesome, and free for public projects. It's saved my arse more than once (my day job source code is managed there).

Chrome.... caching... ugh...
 
I prefer Chrome on OS X (my primary home computer). I used to use Firefox, but it got bloated and crappy.

One downside to SteelSeries...the iPhone doesn't seem to like it. No matter. I wasn't all that serious about a mobile brew monitor, but I can always make a simpler page that doesn't have fancy markup.

SourceForge is an awesome resource. I don't have much code to share, but maybe I should set up a project.

Guess it was my hotmail account that prevented me from placing the sample order with Maxim. Funny, I've ordered from them several times with no issues at all.
 
Chrome is my browser choice too, but, it's caching drives me crazy at times.

SF is great, however, the version control of github is a life saver (at times). We used to run our own cvs servers internally, but moved over to github about two years ago and over-joyed since.

I use a flip phone. The kind with three letters to the number. Yea, dinosaur phone. :) I turn 50 this summer, so maybe I'll break down and switch over to a "smart" phone. They are cool, but big. Ugh...
 
One downside to SteelSeries...the iPhone doesn't seem to like it. No matter. I wasn't all that serious about a mobile brew monitor, but I can always make a simpler page that doesn't have fancy markup.

or... have the code SMS/email you if/when something is out of range. The eye candy is nice, but, you have to be watching it to know anything. Let your "sentry" watch it and tell you when bad things happen. The code I posted to github provides this functionality (cut & paste it out).

Another option is to sign up with Twilio, and process inbound SMS (you can't process inbound SMS using above method). You could then send your computer a msg asking what the temps are, and have the code return a list of temps. Or whatever. At 0.01 per SMS, it's cheap to play. :)
 
That is really cool stuff. If I get done with my 'job' at work today, maybe I'll do some more research into this stuff. Remember, I'm basically starting from scratch, but if I think I can do it, I may goof with it and impress my friends with some cool stuff.
 
Job... LOL. We're launching new product Tuesday and I'm fooling with this stuff... It's all Yuri's fault. I was doing fine until he posted all those super cool controls. Yea, I'll blame him. :)

The worse part is that I may have gone over a cliff. Yesterday I dusted off some daemon code and started morphing it into a server for this stuff.

The "problem" with all these fancy browser screens is that nothing happens when those fancy browser screens aren't running, and, if the machine reboots what then? That's why cron jobs are so sweet - they just do their thing, screen no screen, reboot ok. But, if a cron job is controlling the temps in my fermentation fridge, then I can't change high/low setpoints via a GUI. And I can't turn it on/off via GUI (without stepping toes in the background). In order to achieve autonomous operation that allows for user input, I have to have a background process that controls everything, but has an interface to the rest of the world.

I have the daemon running and controlling fermiLab, but I don't have the new (tcp) interface to the PHP code finished. I'll try to knock that out today/tomorrow, but the day job is going to be busting my you know what for a few days. BTW, It reads an ini file for config - no spooky cron configs and no programming required. :)

Code:
#
# beer server config file
#
[FermilabTemp]
type = ds18s20
device = /mnt/fermiTemp
low = 65
high = 67
switch = FermilabSwitch


[FermilabSwitch]
type = ds2406
device = /mnt/fermiSwitch
 
The "problem" with all these fancy browser screens is that nothing happens when those fancy browser screens aren't running, and, if the machine reboots what then? That's why cron jobs are so sweet - they just do their thing, screen no screen, reboot ok. But, if a cron job is controlling the temps in my fermentation fridge, then I can't change high/low setpoints via a GUI. And I can't turn it on/off via GUI (without stepping toes in the background). In order to achieve autonomous operation that allows for user input, I have to have a background process that controls everything, but has an interface to the rest of the world.

It's important to separate the interface from the "business logic". Then you can have multiple interfaces!

Have you thought about using a database? I've been mulling over logging to a MySQL database, and using the db to store configuration and state information.

I've ordered my free samples from Maxim, and have installed the appropriate software on my server. I'll probably much around with some basic environmental observational stuff first, since I don't have brewing hardware in a "ready-to-go" state at the moment. It'll be a good opportunity to build up a core library of code and functions.
 
It's important to separate the interface from the "business logic". Then you can have multiple interfaces!

Yup, but that's not the "interface" I'm referring to. You're thinking too high-level. :)

Have you thought about using a database? I've been mulling over logging to a MySQL database, and using the db to store configuration and state information.

IMHO, daemon's need an easier config method than a sql db that has no user interface for same. We don't want to force someone to use HeidiSQL (or similar) to config the daemon. Being "old school", mine uses a text file. Logging to mysql... That's a bucket of worms right there. Pro's/Con's. I'll probably be adding support, but I want to finish the core module first.

since I don't have brewing hardware in a "ready-to-go" state at the moment. It'll be a good opportunity to build up a core library of code and functions.

Yea, that's where I'm at too. I have a garage full of a partially built HERMS system. I'm not real keen on finishing it until I can control it. That's how all this started. :)
 
IMHO, daemon's need an easier config method than a sql db that has no user interface for same. We don't want to force someone to use HeidiSQL (or similar) to config the daemon. Being "old school", mine uses a text file. Logging to mysql... That's a bucket of worms right there. Pro's/Con's. I'll probably be adding support, but I want to finish the core module first.
Definitely an idea that I'm going to have to address later on.

One thing I've been pondering is PID control in this system. I've found a totally excellent PDF called "PID without a PhD" that includes some pseudocode for a software PID, and some guidance on tuning performance.

GatorDad said:
Yea, that's where I'm at too. I have a garage full of a partially built HERMS system. I'm not real keen on finishing it until I can control it. That's how all this started. :)
Heh. Now I know I'll need to get the electrician to help me fish some Cat5 to the garage when I get him to run my 50A service for the brewrig. Project creep! :D :mug:

Oh well. I just moved into a new house, and need to collect some environmental data anyway. :D
 
I've already got a few PID routines from previous projects (tuned via simplex).

The "problem" is that it'll have to be implemented via a time slice since 1wire vpots aren't avail. The daemon is threaded already, and the adding another thread to monitor PID activity wouldn't be rocket science. However, for me, I doubt I fool with PID. I'm doing the 10gal Rubbermaid thingy and it's not like temps are going to be wandering all over the place during that 60min. Time will tell - I'm not going to try and solve a problem I don't have in front of me. :)
 
I didn't really understand that, either. The HTML I've been posting is simply for viewing data. There is no process control there. I'll have scripts for that.
 
Not for process control. Example: put an on/off button on the previously posted screens. It won't work if the device is being controlled by a background cron script. The browser app turns the switch on (or off), but the cron script has no idea about this and proceeds to do what it does. Lots of fun ensues from here.
 
Interesting problem, no way to capture events or transfer values between browser and control script then?.
I have used the PID pdf code example as a basis for building pid loops, but had to add some additional code to make it work when you had different spans (small) .13 -1.3 GPM , (large) 70 - 250 deg.F, with varying response times for each loop. Storing the tuning parameters in MySql tables for each loop and operating scenario helped to achieve repeatable results as you switched operating modes and reloaded the optimal tuning values into the loop.
 
Interesting problem, no way to capture events or transfer values between browser and control script then?
You can do both. It would just be poor practice to rely on remote browser input as the sole means for process control. Toggling a mode or changing a value via the browser is fine and perhaps even desirable behavior. It just shouldn't be the primary means by which the rig is managed, and there should be no negative/irrecoverable implications should browser connectivity be lost.

Here's a bash script to generate a simple xml file that is compatible with the browser page above:
Code:
#!/bin/bash

# read all files in a directory
# output their contents to an xml file

# create the xml header and root node
xml="<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"
xml=$xml"<!-- Created by update_xml.sh bash script -->\n"
xml=$xml"<brewhut>\n"

# each file becomes a child element with the filename as its tag
files=/mnt/onewire/links/*
for f in $files
  do
    fname="${f##*/}"
    xml=$xml" <"$fname">"`cat $f`"</"$fname">\n"
  done

# close the root node tag
xml=$xml"</brewhut>"

# output the file
echo -e "$xml" > /var/www/xml/brewhut.xml

The files contained in /mnt/onewire/links:
Code:
lrwxrwxrwx 1 root root 29 Jun 10 21:18 boilTemp -> /home/yuri/testfiles/boilTemp
lrwxrwxrwx 1 root root 28 Jun 10 21:19 boilVol -> /home/yuri/testfiles/boilVol
lrwxrwxrwx 1 root root 29 Jun 10 21:18 mashTemp -> /home/yuri/testfiles/mashTemp
lrwxrwxrwx 1 root root 28 Jun 10 21:19 mashVol -> /home/yuri/testfiles/mashVol
lrwxrwxrwx 1 root root 31 Jun 10 21:19 spargeTemp -> /home/yuri/testfiles/spargeTemp
lrwxrwxrwx 1 root root 31 Jun 10 21:19 steamPress -> /home/yuri/testfiles/steamPress
lrwxrwxrwx 1 root root 30 Jun 10 21:19 steamTemp -> /home/yuri/testfiles/steamTemp

The resulting xml file:
Code:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!-- Created by update_xml bash script -->
<brewhut>
 <boilTemp>10</boilTemp>
 <boilVol>10</boilVol>
 <mashTemp>10</mashTemp>
 <mashVol>10</mashVol>
 <spargeTemp>20</spargeTemp>
 <steamPress>10</steamPress>
 <steamTemp>10</steamTemp>
</brewhut>
 
You can do both. It would just be poor practice to rely on remote browser input as the sole means for process control. Toggling a mode or changing a value via the browser is fine and perhaps even desirable behavior. It just shouldn't be the primary means by which the rig is managed, and there should be no negative/irrecoverable implications should browser connectivity be lost.

You can't do both without clashes. Thought experiment:

  • You have a device that is controlled by cron script.
  • The cron job fires up once a minute.
  • You manually turn off the device with a browser control (you have your reasons).
  • The cron job fires up 42 seconds later and turns the device on - and you don't want it on.
 
It sounds like it should be possible to create a common file that would increment a watchdog value only if the browser was alive, and the back ground process would shutdown when watchdog value did not change. If the common file maintained a record of where you were at in the brewing process then the background process could resume operation when watchdog value begins to increment again.
 
Oh my, those types of solutions never work out. Been there, done that, bought the video tape AND the tour package. :) This problem is already solved guys (see prior notes on daemon). I'm working out the JSON interface to the PHP code now...
 
Food for further thought...not the best example since I have to physically throw a switch, which could be detected automatically, but follow me through the logic (assuming there is no automatic mode detection capability):

I have only one set of SSRs to control two separate heating elements. During the mash, I control the heating element in my steam generator. When it's time to boil, I throw a DPDT switch to use the same SSRs to control the heating element in my boil kettle.

In steam mode, I measure pressure and temperature in the steam generator, and use those inputs as the trigger to switch the element on or off.

In boil mode, I measure temperature in the boil kettle and begin to throttle back the duty cycle on the heating element as boiling temps are achieved.

I could use the browser to select boil vs steam mode without any foreseeable negative consequence. The cron job would take the mode into account when dealing with the SSRs.

By the same token, the browser could be used to dictate "off" and "automatic" modes of operation. If the cron job sees "off," it sets relay output to off and exits. Again, no negative consequences.
 
I don't think that's a valid use case. As I've state a few times already, the task I'm working on is *not* for process control. You're outlining the process control of making a batch - and I would never use cron to control that process. :) It's a procedural task.

For controlling a few fermentation chambers, you can't have a cron job diddling around in the background if the user is turning things on/off via a browser control...
 
I'm going to convince you that there is a case for browser control...even if you choose not to use it :D

You're on a business trip. It's time for a diacetyl rest in your lager chamber, and you realize that you planned poorly and had the lagerator set to maintain a constant cold temp. What's the harm in allowing the browser to add a time/temp to the schedule?

Yeah, yeah, yeah, you could just ssh into your machine and accomplish the same thing...assuming you have a remote client handy.
 
Oh - we're on different pages, but agree!

What I'm working provides browser control - without screwing up the backend. :)
 
There's nothing to be conceded. I can't stay up to 2am like I used to... Geez I'm getting old (I used to code all night, going to bed at daybreak). :mug:
 
SCORE! I just bought a CO2 monitor for $10 at a yard sale. And, it outputs voltage proportional to the CO2 ppm. As in I can put this creature in my keezer, monitor the CO2 level and make lots of noise if it detects a strong rise in CO2 levels ("Your CO2 is leaking you dummy - go fix it before it drains empty"). :)
 
Back
Top