Introduction
Microwave ovens can actually be fairly sophisticated cooking devices. Unfortunately taking full advantage means that the user has to enter a long and often counter-intuitive chain of power settings, times, and pauses. Ultimately most users simply hit 1 minute and then check to see if the food either needs another minute or is already overcooked. There HAS to be a better way.
Proposal
Our project uses a combination of: a microwave oven that has been hacked to give it Internet access, an Azure database of microwave ovens, pre-packaged foods, recipes optimized for the food/oven combination, and a smartphone app that scans the barcode on the food to be heated or cooked. This is a team project and my son Canin will manage the Azure programming as well as a sample Android app. I will be working on the hacking the microwave and coding its Internet enabled application.
Architecture Overview
We based our design on a Message Queue pattern rather than a traditional restful architecture for several important reasons. Estimates say that by 2020 there will be over 50 billion appliances connected to the Internet, all generating copious amounts of messages. Some of these messages will be status updates without acknowledgement while others will be of an interactive nature and all will be connected to cloud based services like Microsoft Azure. Utilizing a message queue broker as an exchange point, tying producers and consumers together promotes complete decoupling of the wide variety of resources while allowing the protocol itself to scale internal resources like queues and exchanges according to load.
We had to use RabbitMQ as our message broker rather than the Azure Service Bus for several reasons: We had a familiarity with RabbitMQ and the scope of the project imposed time constraints on learning the Azure Service Bus architecture, also Windows 10 has not been released for the Raspberry Pi and the JavaScript client for Service Bus is still maturing. However, since the architecture is message based, there should be no problem migrating to Azure Service Bus when the tools are in place.
Each microwave will have a QR code that contains a JSON object with two fields. A GUID that is used to register and uniquely identify the appliance to the Azure service and an optional appliance identifier (brand, model). This gives the manufacturer two options. Preferably, they can use the GUID to register each appliance with the cloud service. This would not only associate the capabilities (power, sensors, etc.) of the appliance for proper menu selection but it could also track manufacturer’s registration of the oven to the owner for warranty, product recalls, or required maintenance. The other option is for the manufacturer to create a GUID for later registration with the cloud service by the owner, as well as the optional brand/model field which would be used for the proper menu selection.
RabbitMQ supports a topic based queueing model that allows hierarchical routing of message queues. We are using this feature to control distribution of diagnostic error codes. The format of this message is:
manufacturer.classification.model.[hardware/software].[info/warning/critical]
This allows different interested parties to subscribe exactly to the information. For example, the manufacturer may want all hardware error codes by subscribing to “ACME.*.*.hardware.critical”. They could also get software warnings for specific models by subscribing to “ACME.Appliances.B7633S2.software.warning”. If the appliance was registered by the owner, the regional repair facility could receive the code and, if under contract, the part could be automatically ordered and sent to the local dealer. Because of the unique abilities of message queueing, any number of consumers can subscribe to the messages and the service can be dynamically scaled.
The Thing
This is an IoT contest of course and the “Thing” in our project is a microwave oven. Any microwave is essentially a processor using a keypad and LCD display as the UI. A user “programs” the processor through a series of button presses appropriate to the food they are cooking. Many foods have a recommended procedure for heating/cooking but I rarely see anyone in the break room at work actually following the procedure, usually opting for the press-and-pray approach. A commercially produced IoT microwave would have an additional Internet enabled data channel that would act as an alternative data entry port. For this project we hacked a craigslist microwave using a Raspberry Pi 2 Model B, programmed in Python as the controller. I (Todd) was hoping that Windows 10 support would be available, but I had to resort to programming in Python. I say “resort” because I’ve never used Python so had to learn Google as I went. Therefore, the code is a bit of a hack for now, but will be re-written with the release of Windows 10 support on the Pi.
The cover was attached by a number of security Torx screws for a reason, so this is probably a great time for a disclaimer.
CAUTION: The reason there are security screws is because microwaves use LETHAL VOLTAGES in their operation and should never be opened and modified unless you know EXACTLY what you are doing. Even unplugged, they use a large high voltage capacitor that can maintain a charge and must be safely discharged before doing anything else. If you don’t know what I’m talking about, you shouldn’t remove the cover.
The Pi establishes a connection with a message queue on Azure to receive, authenticate, and interpret incoming encrypted commands. Initially I was going to use a solid state crossbar switch to emulate button presses. Several attempts failed, possibly because of the added capacitance of the extra circuitry.
I finally decided to abandon the emulation, completely remove the control board and write the entire microwave operating system on the Pi. I retained the original keyboard, but had to “dremel-in” a new LCD display as well as building a control board with the high current relays for the light/fan/turntable and the magnetron. The specialized connectors for the keyboard, relay control and safety interlock switches were scavenged for use.
One of the advantages of a cloud based appliance is its ability to perform diagnostics and report failures. The microwave has a high current fuse, a cooking cavity flame sensor and a magnetron over temperature sensor. These three items are in-line with the 120 volt supply voltage and any can cause the microwave to appear “dead.” The control board does not rely on this main power buss so can remain powered even in the event of a sensor detect. I needed a simple way to detect loss of power at the three monitoring points, so I found some surplus cell phone chargers and removed the circuit boards. The 120 volt inputs of each were tied to their respective monitoring point and their 5 volts (through a 1K resistor since the Pi is 3.3 volt) went to the Pi. Loss of power on these detectors results in a falling edge which generates an interrupt and sends a message. For demonstration, I removed the magnetron over-temp sensor lead and attached an external pushbutton to simulate an event.
This is a schematic diagram of our conversion.
Final version of the microwave. The control panel is removeable so to make development easier we created external connections for power and HDMI. The oven program is not set to auto-start so a wireless keyboard and external monitor is used to run the application.
Azure Cloud
Azure will maintain a database of microwave brands, models, and an enumeration of their supported commands, as all ovens won’t support all available commands. It will also contain the UPC codes of pre-packaged foods and the associated recommended heating procedures. This database could be expanded to allow users to vote on the quality of a recipe as well as submit their own for a particular food. Although not a part of this project, the Azure Machine Learning API could be used to verify the commands sent to the microwave to prevent damage to a microwave either accidental or malicious.
The database will also be used to authenticate the users microwave and mobile scanning app. This authentication will then establish the destination of the microwave command set after scanning and also prevent hackers from sending malicious commands to unauthorized devices.
The Mobile App
The Android application will act as a barcode scanner to transmit the UPC of the item to be heated. It will also be authenticated and associated with the users account on Azure as well as authorizing their microwave oven. The app could also be used to rate the particular recipe being used.
Messaging and Communication
One of the key architecture choices on the project was the means of communication between the various elements. To this end we picked the Advanced Messaging Queuing Protocol (AMQP) message broker RabbitMQ. This open source software provides an abstracted middleman for the data, allowing for coder friendly, efficient, and easily expanded and scaled messaging, which collectively was well befitting a cloud environment.
RabbitMQ works based on exchanges and queues. Producer end points publish directly to a given exchange, which may either publish straight to a specific queue, or follow binding logic. Consumer endpoints can either consume from a predefined queue, or create an exclusive queue and bind input to a queue through routing logic. The configuration choice depends entirely on what is attempting to be accomplished.
The broker manages messages that must be acknowledged (ACK) or negatively confirmed (NACK, also the default behavior upon errors such as disconnection or timeout), ensuring that messages erroneously consumed will find the intended recipient, providing a robustness to the communication. In some parts of our project this was important (communication to and from the cellphone app) and at other times it was not needed (when the Microwave is processing cooking instructions, new cooking instructions are assumed to be erroneous, and thus can be safely discarded).
There are also advantages with RabbitMQ as it supports a number of programming languages, including Python, C#, and Java, all three of which were used in this project. Beyond these three it also supports Perl, Ruby, Javascript/node.js, Erlang, and Clojure, lending itself extremely well to cross-platform demands; while C# was a fantastic choice for a windows VM and for speaking to SQL Server via Entity Framework, Python was the clear choice for the Raspberry Pi, and Java was the native language for the Android app.
RabbitMQ further supports authentication, and it extends all management of itself, its users, its queues, and its exchanges via an API. While the API extends outside of the scope of our project, it does allow for automated configuration as a user base scales up or down in a Cloud environment.
We’ve found RabbitMQ to be a fantastic piece of technology. If you’re interested in the AMQP model as a whole, or implementing RabbitMQ in the various languages, the official website is a great place to get stated. A quick reference guide for AMQP can get you started, and then you can dive into the six tutorials in the language of your choice. Of course, naturally, you can keep reading in this article to see how we implemented the broker.
To further abstract the messaging system—allowing new modules to be later added that can consume and publish to the exchanges and queues respectively—we elected to employ the very common data notion, JSON,. Due to its wide-spread popularity, JSON is extremely easy to find third party libraries (or even core libraries) that will serialize and reserialize data objects into this notation. This means you can have faith that future code can easily consume these objects in—by large—the language of your choice.
For security, then, the JSON can be encrypted, as we have done with the server to microwave communication.
Using the code
Android App
The Android app was designed to illustrate the connection to the cloud. The Android app is a key part of the user interaction, and was designed to illustrate the connection to the cloud. The app allows a user to scan one’s food, verify cooking instructions, and then push them to the microwave.
The app provides example of two different paths of communication: first, it shows a Remote Procedure Call (RPC) where the phone makes a request (barcode scan request for cooking instructions for the given microwave), and then a more standard work queue where the approved cooking instructions are pushed to the server. Both interactions are with the VM server (specifically with the C# code), but the RPC is somewhat more complicated because it not only is a publisher, but the consumer of the return message.
We abstracted this communication in one method, which I’ll break down. (As a side note, I won’t be including the entire method in this article, but you can find it in the source code download)
First note that I abstracted the return type as “T.” This allowed the method to not care what message object I was receiving from a possible queue, only needing specified when this method was called. Although the method was only employed twice, it could easily be implied again for, say, login calls to the server, requesting microwaves registered to the account, registering a new microwave, or rating a cooking instruction recipe.
public <T extends BaseMessagingObject> void sendToRabbit(String message, String queue, boolean isRpc, Class<T> returnType ) throws Exception {
String returnTypeName = "null";
Next is where the code sets up the connection to the RabbitMQ server. No exchange or queue has been specified; this is just the initial connection.
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
factory.setPort(PORT);
Connection connection;
connection = factory.newConnection();
Channel channel = connection.createChannel();
Next the code declares the queue. Declaring a queue or exchange to the server is literally requesting the object to be created. RabbitMQ is nicely setup where if you declare a queue or an exchange that already exists, and do so with the parameters that are already present, it will ignore your request silently. If you declare one that exists with different parameters it will inform you with an error. This makes it safe for code to always declare a queue or exchange it will be dealing with to ensure it exists, and while doing so, ensure the exchange or queue is as it is expecting.
Since our queues are static, we didn’t need to catch an error for a duplicate queue with different parameters, and thus no error catching was used for the queue declaration.
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Next various properties are setup. Most of these are stubbed out, because they’re only used (or at least, not changed from default values) if the call is an RPC call. Among these are a correlation ID. A correlation ID allows an RPC call’s reply to be matched up with the original request. This is important if you send a batch of work at an exchange at once (thus quite possibly processed by many different programs or threads, and returned to you at different times) and it’s important to match up the results with the original requests. For our project, we just use the correlation ID to confirm that singular message we were expecting to return.
Log.d("code", "Queue declared");
AMQP.BasicProperties props = null;
String corrId = null;
QueueingConsumer consumer = null;
String response = null;
Log.d("code", "Naming Exchange");
String exchangeName = "";
String excahngeType = "";
if(isRpc) {
Log.d("code", "Starting RPC settings");
corrId = java.util.UUID.randomUUID().toString();
String replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true, consumer);
props = new AMQP.BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName).build();
Log.d("code", "Ending RPC settings");
}
else
{
Log.d("code", "Not RPC so won't set those settings");
}
Next we move onto the actual event, publishing the code. The way specifically the phone handles the publications it never needs to bind or route the messages published, nor make use of any more advanced features of RabbitMQ, keeping the call generic and simple. You'll see routing and binding later with the server/microwaver interaction.
Log.d("code", "About to publish. exchangeName: " + exchangeName + " QueueName: " + QUEUE_NAME);
channel.basicPublish(exchangeName, QUEUE_NAME, props, message.getBytes());
Log.d("code", " [x] Sent '" + message + "'");
And the message is on its way to the exchange and (within a millisecond or two!) the queue.
Next, if it’s an RPC, it’ll enter into this simple loop. A while(true){ … } keeps it running until we explicitly request it to break. Inside the loop it looks for a message to be published to its unique reply queue (generated above), and checks the correlation ID. While it’s practically improbable—even virtually impossible—for a message to be mistakenly published to a custom return queue, it’s still a best practice to verify the dequeued message is the one we were expecting.
Our code then makes use of a simple return message class. The class allows for a genericized history and retrieval of messages. While I won’t go over the simple class, its main purpose was to allow for ease of multithreading (the server connection is called as an asyc task) through a managed simpleton pattern. This class could be expanded to a publisher/subscriber pattern, so the application could listen in on new updates.
while (isRpc) {
Log.d("code", "Waiting for return message");
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
Log.d("code", "Got message, checking if it matches corrID");
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
Log.d("code", "Found correct message");
response = new String(delivery.getBody());
ServerReturn<T> serverReturn = new ServerReturn<T>();
serverReturn.MessageClass = returnType;
serverReturn.FromQueue = QUEUE_NAME;
serverReturn.Timestamp = new Date();
serverReturn.Message = response;
Log.d("code", "Adding to server return");
if(serverReturnManager == null)
Log.d("code", "serverReturnManager is null!");
serverReturnManager.addItem(serverReturn);
break;
}
}
Log.d("code", "Cleanning up RabbitMQ");
channel.close();
connection.close();
Log.d("code", "Cleaned up RabbitMQ");
}
And that concludes the messaging of the Android app. While the app could easily be expanded with additional calls to the server via additional features, these two types of calls--work request and procedural--illustrate both the cloud integration, and how decoupled communication needs to be.
Azure Cloud
The Azure cloud was used in three distinct ways: To host the RabbitMQ server (on a Windows Server VM), to host the C# code (via the same Windows Server VM), and to host the data layer (via a cloud-based SQL Server instance).
RabbitMQ
The RabbitMQ server we used is a fairly straight-out-of-the-box variety. I would highly recommend installing the RabbitMQ Management Plugin (which includes a web interface) if you want to play around with RabbitMQ, as it makes troubleshooting, visualizing, and performing basic administration of the RabbitMQ server, its queues, exchanges, users, and processed messages vastly easier.
We registered a user for the C# code, and registered users for the Java access device (cellphone) and Python hardware object (microwave) under their respective GUIDs. Keeping logins unique allows for additional security, and auditing of the system if this was to be actually deployed. For management reasons, I suggest tagging any user-unfriendly account names with friendly tags.
The interesting bits of RabbitMQ in our project have less have to do with the server install, and more how our code uses RabbitMQ to interact with the cloud, so I won’t dwell on the messaging broker server itself here.
SQL Server
SQL Server was selected both to its Azure support and the fact that it communicates easily with C# via Entity Framework. We used a code-first approach to the database, structuring our data objects in C# and then pushing them as tables using Entity’s built in logic.
For a developer who’s looking for an easy, agile way to construct a database, and one that supports a coder’s mindset I highly recommend Entity framework. While it does have its own limitations and nuances, it’s great for spinning up powerful apps quickly, especially in small groups like our own.
C# or ”Our Cloud’s Heart”
C# was used to create the code-based server, which acted as both the hub of all communication, and the access to the database. By employing a relational database, SQL Server, for the data storage, and RabbitMQ for the messaging, the server is abstracted, even from instances of itself. To illustrate this point, the server can be fired up multithreaded or as multiple instances across multiple VMs and run in parallel.
There’s quite a bit going on in the C# code (including everything from the data and messaging objects, to the interaction with Entity Framework to the multithreaded worker management), but I’m going to highlight the RabbitMQ communication, specifically with the cellphone to sever communication.
If you’re looking alongside in the source code, this is specifically in the CookingInstructionQueue class.
First it tries to dequeue the message. I setup a special method that dequeues with a timeout, surrounded by a try/catch called DoesTimeoutForMessageWait(int, out BasicDeliverEventArgs, SmartNukeQueueDescription). You don’t need to use a timeout like I did (without it, the code will simply continue to wait forever for a message in the queue), but timing out allows for a couple of advantages. First, timing out allows the thread processing the method to safely check if a cancerization was requested, and second, sometimes the dequeueing hangs on a closed connection, and timing out allows you to repair/reestablish the connection if needed.
try
{
if (!SmartNukeQueueLibrary.DoesTimeoutForMessageWait(timeout, out basicDeliverEventArgs, _fromInputQueue))
{
Console.WriteLine(_fromInputQueue.QueueName + ": Started processing message.");
Next I call a simple deserialization method that depends on the class library JSON.Net. The method allows me to select to deserialize a message as JSON (the one used for this project), BSON, or not deserialize at all. BSON is an advancement on JSON that allows for more complex data being transferred (treating it closer to the binary information, and less like text), and is clearly with its advantages, but it wasn’t requires in this project.
CookingInstructionConfirmation confirmationMessage =
SmartNukeQueueLibrary.Deserialize<CookingInstructionConfirmation>(
Encoding.UTF8.GetString(basicDeliverEventArgs.Body),
SerializeTypes.Json);
Console.WriteLine(_fromInputQueue.QueueName + ": Deserialized.");
Next a simple authorization is done. It checks if the access device GUID is associated with the microwave GUID. While this could easily be faked in a message, the sheer volume of possible GUIDs makes a random GUID collision virtually impossible, and the chance to have a double collision (on a registered microwave and a connected access device (cellphone)) even more so astronomically unlikely. As a side note we acknowlede this shouldn't be the only authorization done in a production app, but it is an instance of authorization that can be done.
It’s worth mentioning that an important advantage of GUIDs is that it allows distributed registration of entities; since the chance of GUID collisions are literally astronomically low, in fact, to borrow a colorful description from Wikipedia...
Quote:
For example, consider the observable universe, which contains about 5×10^22 stars; every star could then have 6.8×10^15 universally unique GUIDs.
Or, to quote Tom Ritter on StackOverflow.com (http://stackoverflow.com/questions/184869/are-guid-collisions-possible)
Quote:
For a 1% chance of collision, you'd need to generate about 2,600,000,000,000,000,000 GUIDs
You can have the IDs generated in a distributed manner with no practical concern for duplication. This works very well with an Internet of Things distributed model, and is also why in part Active Directory makes use of GUIDs. But I digress, back to the code...
if (DbManager.IsDeviceAuthorizedForDevice(confirmationMessage.OriginalRequestMessage.MessageCreatedBy,
confirmationMessage.OriginalRequestMessage.MicrowaveID))
{
var cookingInstruction = DbManager.GetCookingInstruction(
confirmationMessage.OriginalRequestMessage.MicrowaveID,
confirmationMessage.OriginalRequestMessage.Barcode);
Console.WriteLine(_fromInputQueue.QueueName + ": Got cooking instruction.");
Ad this point either the instructions were pulled down, or an error message was generated. To clarify, the instructions are found via a database lookup matching on the food’s barcode, and the model of the microwave in question.
The return message is then populated...
MicrowaveCookingInstructionMessage message = new MicrowaveCookingInstructionMessage
{
Instructions = cookingInstruction.CookingInstructionEvents,
MessageCreatedBy = ServerProperties.ID,
MicrowaveGuid = confirmationMessage.OriginalRequestMessage.MicrowaveID,
Timestamp = DateTime.UtcNow,
Title = cookingInstruction.FoodItem.FoodName
};
Console.WriteLine(_fromInputQueue.QueueName + ": Ceated return message.");
Serialized (a mirror method of the deserialization method used earlier)...
var serializedCookingInstruction = SmartNukeQueueLibrary.Serialize(message,
SerializeTypes.Json);
Console.WriteLine(_fromInputQueue.QueueName + ": Serialized.");
And encrypted. AES encryption—with a unique vector (or IV) per message—is used, which secures the communication. We used this to show that messages could easily be secured for transmitting within the messages themselves, along with any network-based security. If this was expanded to disposable keys or timestamp/ID tracking, duplicate message injection could easily be avoided.
var encrypted = MessageEncryptionManager.CreateEncryptedMessage(serializedCookingInstruction,
confirmationMessage
.OriginalRequestMessage
.MicrowaveID);
Console.WriteLine(_fromInputQueue.QueueName + ": Encrypted.");
And the code is published! All and all the communication with RabbitMQ works extremely similar to what you see in Java, but I coded it in two different manners to show the flexibility in even simple application; once you understand RabbitMQ for one language/driver, moving to other languages and drivers becomes a fairly simple and straight forward process. Did we mention that we like RabbitMQ?
You’ll note that we publish with the routing key being the microwave’s GUID. The listening microwave has already bound an exclusive, custom queue to the exchange with a routing key of the microwave’s GUID. RabbitMQ then copies the message from the exchange to the queue, so the microwave can consume it. In this manner, the message is routed to the correct device, and as many microwaves as needed can be fed from a single exchange with no concern for the messages being lost or an incorrect messages reaching the wrong microwave.
Console.WriteLine(_fromInputQueue.QueueName + ": Publishing.");
_toHardwareObjectQueue.BasicPublish(Encoding.UTF8.GetBytes(encrypted),
routingKey: confirmationMessage.OriginalRequestMessage.MicrowaveID.ToString());
Console.WriteLine(_fromInputQueue.QueueName + ": Published.");
}
We now get an error message if somebody requested a cooking instruction from an invalid access device GUID, for an invalid microwave GUID, or an invalid combination of access device and microwave. Since the combination of access device/microwave should have been A) established at login, and B) confirmed upon the initial scan request (which returns an error to the user upon authorization failure), it’s assumed that any failure at this point is a hacking attempt, and so no response should be delivered to whomever injected it.
else
{
Console.WriteLine("Unauthorized attempt from (as claimed, but unconfirmed) AccessDevice ID: " + confirmationMessage.OriginalRequestMessage.MessageCreatedBy + " for hardware device " + confirmationMessage.OriginalRequestMessage.MicrowaveID);
}
Next ACKs are distributed.
if (_fromInputQueue.Channel.IsOpen)
{
_fromInputQueue.Channel.BasicAck(basicDeliverEventArgs.DeliveryTag, false);
}
else
{
_fromInputQueue.Initialize();
}
if (_toHardwareObjectQueue.Channel.IsOpen)
{
_toHardwareObjectQueue.Channel.BasicAck(basicDeliverEventArgs.DeliveryTag, false);
}
else
{
_toHardwareObjectQueue.Initialize();
}
}
And this piece of code is an else based off the timeout. If it timed out, the connection is closed (if open) and reestablished in case there is a network hiccup that disturbed the connection, causing the timeout. This isn’t strictly required (RabbitMQ has yet to lead us astray in claiming it was connected when it was not, in fact, connected), but it’s easy to implement, and takes virtually no resources to accomplish.
else
{
Console.WriteLine(_fromInputQueue.ExchangeName + " did timeout.");
_fromInputQueue.Initialize();
_toHardwareObjectQueue.Initialize();
}
}
If a significant failure happened, the code attempts to NACK (negative acknowledge) the message, so RabbitMQ will requeue the message. We then go on to re-initialize the queue, which disconnects our server from the RabbitMQ server, thereby NACKing any standing messages consumed by our server by default. Thus, our NACK call is redundant, but it’s still good practice to be clear with your intentions and messaging.
catch (Exception ex)
{
try
{
_fromInputQueue.Channel.BasicNack(basicDeliverEventArgs.DeliveryTag, false, !basicDeliverEventArgs.Redelivered);
}
catch (Exception exInner)
{
Console.WriteLine(exInner);
}
Console.WriteLine(ex);
_fromInputQueue.Initialize();
_toHardwareObjectQueue.Initialize();
}
And that’s the basic communication method for one of the communication cases; the cooking confirmation was dequeued and deseriailzed, then it was processed via an SQL server lookup. A new message was created, serialized, and encrypted. The message was then pushed to an exchange that all microwaves have bound to, and the original command request message was ACKed from the original queue that this method consumed from. Pretty snazzy.
Microwave
#setup door interlock -- ground when door opens
GPIO.setup(doorSwitch, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(doorSwitch, GPIO.BOTH, callback=door_open_callback, bouncetime=300)
# setup flame sensor
GPIO.setup(flameDetector, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(flameDetector, GPIO.FALLING, callback=flame_callback, bouncetime=300)
# setup magnetron over temp
#
#this is the correct setup for the sensor
#
#GPIO.setup(magnetronOverTemp, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
#GPIO.add_event_detect(magnetronOverTemp, GPIO.FALLING, callback=magtemp_callback, bouncetime=300)
# This setup is for demo using external trigger switch
GPIO.setup(magnetronOverTemp, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(magnetronOverTemp, GPIO.FALLING, callback=magtemp_callback, bouncetime=300)
# setup fuse failure detect
GPIO.setup(fuse, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(fuse, GPIO.FALLING, callback=fuse_callback, bouncetime=300)
# ------------------------------------------------------------------------
# flame_callback
# ------------------------------------------------------------------------
def flame_callback(channel):
send_email("Flame Sensor Alarm", MICROWAVE_GUID, CELL_TO_ADDRESS)
send_diagnostic("Hardware", "ERROR", MICROWAVE_GUID + ": Flame Sensor Alarm")
return
# ------------------------------------------------------------------------
# magtemp_callback()
# ------------------------------------------------------------------------
def magtemp_callback(channel):
#send_email("Magnetron Overtemp Alarm", MICROWAVE_GUID)
# Send Text as part of demo
send_email("", MICROWAVE_GUID +" Magnetron Overtemp ALarm", CELL_TO_ADDRESS)
send_diagnostic("Hardware", "ERROR", MICROWAVE_GUID + ": Magnatron Overtemp Alarm")
return
# ------------------------------------------------------------------------
# fuse_callback()
# ------------------------------------------------------------------------
def fuse_callback(channel):
send_email("Fuse Failure Alarm", MICROWAVE_GUID, CELL_TO_ADDRESS)
send_diagnostic("Hardware", "ERROR", MICROWAVE_GUID + ": Fuse Alarm")
return
Interrupts are set for events outside normal operation for events like: door open, and sensor events like flame detector or magnetron over-temp. The interrupt has an associated callback routine that handles the interrupt then returns control to the main applicaiton. A sensor event generates a message routed to the appropriate diagnostic hierarchy message queue.
# ------------------------------------------------------------------------
# connect_azure()
# ------------------------------------------------------------------------
def connect_azure():
print 'in connect'
global channel
global connection
global queue_name
try:
credentials = pika.PlainCredentials(USER_ID, PASSWORD)
connection = pika.BlockingConnection(pika.ConnectionParameters(RABBITMQ_HOST,
5672,
'/',
credentials))
channel = connection.channel()
print "Channel defined"
channel.exchange_declare(exchange = "CookingInstructionCommandExchange",
durable = False,
type = 'topic')
result = channel.queue_declare(exclusive = True)
queue_name = result.method.queue
channel.queue_bind(exchange = "CookingInstructionCommandExchange",
queue = queue_name, #"CookingInstructionCommandQueue"
routing_key = USER_ID)
Define que for diagnostic reports -- setup with routing key
channel.exchange_declare(exchange = "DiagnosticExchange",
durable = False,
type = 'topic')
result = channel.queue_declare(exclusive = True)
queue_name = result.method.queue
channel.queue_bind(exchange = "DiagnosticExchange",
queue = "DiagnosticQueue",
routing_key = create_routing_key())
except:
logging.warning("Can't open RabbitMQ")
return
# ------------------------------------------------------------------------
# receive_from_que()
# ------------------------------------------------------------------------
def receive_from_que():
#print "in receive_from_que"
method_frame, header_frame, body = channel.basic_get(queue_name) #"CookingInstructionCommandQueue"
if method_frame:
print method_frame, header_frame, body
print "Ready to ack"
channel.basic_ack(method_frame.delivery_tag)
return body
else:
return None
The application establishes two RabbitMQ exchanges with "topic" queues, one for receipt of recipes and the other for transmission of diagnostic information. The queue manager is a process running on Azure. This allows complete de-coupling between the microwave and the consumers of the messages.
# ----------------------------------------------------------------
# MainLoop
# ----------------------------------------------------------------
def main_loop():
try:
print "in main loop"
state = "mainLoop"
description = "Burritto"
beep("YourMealIsReady.wav")
update_display(0, "Ready")
while (True):
d = receive_from_que()
if d:
salt,msg,msgl = d.split("&")
msgli=int(msgl)
dd = decrypt_two(salt, msg, msgli)
recipe(dd) #use j instead of d for testing without Azure/RabbitMQ
end_cook()
else:
digit = check_keypad()
if digit == None:
continue
else: manual_entry(d)
time.sleep(.5)
# Send Text
send_email("", description +" is Finished Cooking", CELL_TO_ADDRESS)
except Exception as e:
logging.warning("Main loop try exception:{}".format(e))
GPIO.cleanup()
connection.close()
GPIO.cleanup()
After initialization the microwave sits in a Main() loop waiting for either keyboard input, or a message from the queue. When a message (JSON object) arrives in the queue it is decrypted and sent to the recipe() function.
# -------------------------------------------------------------------
# recipe()
# -------------------------------------------------------------------
def recipe(json):
global done_cooking
global description
done_cooking = False
encoded = Payload(json)
p = decrypt_two(encoded)
description = p.Title
update_display(0, description)
update_display(1, "Start/Clear")
beep("PressStartBeginCook.wav")
# check for kepress
d = start_cancel()
if (d == "cancel"):
cancel()
return
i = 0
try:
arrayLength = len(p.Instructions)
while (i < arrayLength):
# print p.Instructions[i]['ID']
# print p.Instructions[i]['Type']
# print p.Instructions[i]['Time']
# print p.Instructions[i]['DisplayMessage']
# print p.Instructions[i]['PercentagePower']
if (p.Instructions[i]['Type'] == 1): #pause to cool
operationTime = p.Instructions[i]['Time']
update_display(0, p.Instructions[i]['DisplayMessage'])
new_cook(operationTime, power)
elif (p.Instructions[i]['Type'] == 2): #this is a defrost cycle
power = .5
operationTime = p.Instructions[i]['Time']
update_display(0, p.Instructions[i]['DisplayMessage'])
new_cook(operationTime, power)
elif (p.Instructions[i]['Type'] == 3): #This is standard time/power cook
lcd.clear()
operationTime = p.Instructions[i]['Time']
power = p.Instructions[i]['PercentagePower']
update_display(0, p.Instructions[i]['DisplayMessage'])
new_cook(operationTime, power)
#4 reserved for convection
elif (p.Instructions[i]['Type'] == 5): #user interacation
update_display(0, p.Instructions[i]['DisplayMessage'])
beep("PleaseTurnFood.wav")
lcd.clearRow(1)
while (True):
entry = check_keypad()
if (entry == None):
continue
if (entry == stopClear):
cancel()
return
elif (entry == start):
break
else:
#this should beep
print "incorrect key"
i += 1
state = "endRecipe"
update_display(0, "Burrito")
update_display(1, "Is ready")
beep("YourMealIsReady.wav")
done_cooking = True
except IndexError:
logging.warning("Index Error in recipe")
pass
return
# --------------------------------------------------------------------------
# new_cook()
# --------------------------------------------------------------------------
def new_cook(hhmmss, power):
MIN_TIME = 30
update_display(1, hhmmss)
cookTimeSeconds = get_seconds(hhmmss)
if (cookTimeSeconds < MIN_TIME):
onTime = cookTimeSeconds
offTime = 0
else:
onTime = MIN_TIME * power
offTime = 30 - onTime
totalCycleTime = onTime + offTime
startTime = time.time()
timeToStopCooking = startTime + cookTimeSeconds
secondsIntoCycle = 0
# Turn on light/fan/turntable
control_main_relay(1)
# Turn on magnetron and start cooking
control_mag_relay(1)
MagIsOn = True
while (time.time() <= timeToStopCooking):
displayRemainingTime = get_hhmmss(int(timeToStopCooking - time.time()))
update_display(1, displayRemainingTime)
loopStartTime = time.time()
secondsRan = time.time() - startTime
secondsIntoCycle = secondsRan % totalCycleTime
#Turns Mag off if not supposed to be on
if (MagIsOn) and (secondsIntoCycle > onTime):
control_mag_relay(0)
MagIsOn = False
#Turns Mag on if supposed to be on
if (not (MagIsOn)) and (secondsIntoCycle <= onTime):
control_mag_relay(1)
MagIsOn = True
#update timers
secondsLeftToSleep = loopStartTime + LOOP_TIME - time.time()
time.sleep(secondsLeftToSleep)
d = check_keypad()
if (d == "cancel"):
cancel()
break
# make sure magnetron is turned off!!
control_mag_relay(0)
#turn off main relay
control_main_relay(0)
HHMMSS = " "
power = 0
return
The recipe() function uses the recipe portion of the GUID to move through each step of the cooking process.
As recipe() iterates through the command steps each is evaluated for the type of command. . A step is composed of a description, operation type, time, and power level. For example: a cooking step will display “Cooking” with an updated countdown timer while a user interaction will pause, prompt the user to do something like “Turn the food over” or “Waiting for stir” and will continue when the user hits the continue button. For defined cooking events new_cook() is called for each step. Any operation can be canceled by hitting the Stop/Cancel button. At the end of the process the display is updated and, optionally, a text is sent to the phone.
Points of Interest
We made a short video demonstrating our project: http://youtu.be/S2FJmgqd840
History