├── .gitignore ├── CameraIoTEdge ├── Dockerfile └── app │ ├── AzurePost.py │ ├── static │ └── image.jpg │ └── takepicture.py ├── docs ├── acrcheck.png ├── addmodule.png ├── azurecr.png ├── createedge.png ├── dockerimages.png ├── dockerps.png ├── moduledeployed.png ├── moduledetails.png ├── modulerunning.png ├── setmodule.png └── twin.png ├── opencv-330.Dockerfile ├── opencv-341.Dockerfile ├── opencv-342.Dockerfile └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | CameraIoTEdge/app/__pycache__/* 3 | -------------------------------------------------------------------------------- /CameraIoTEdge/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ellerbach/opencv:342 2 | 3 | ADD app app 4 | 5 | RUN apt-get update && apt-get upgrade \ 6 | && apt-get install libboost-python-dev \ 7 | && pip3 install azure-iothub-device-client \ 8 | && pip3 install azure-storage-blob \ 9 | && pip3 install flask \ 10 | && pip3 install numpy \ 11 | && sudo rm -rf /var/lib/apt/lists/* 12 | 13 | # Set the working directory 14 | WORKDIR /app 15 | 16 | EXPOSE 1337 17 | 18 | # Run the flask server for the endpoints 19 | CMD ["python3","AzurePost.py"] -------------------------------------------------------------------------------- /CameraIoTEdge/app/AzurePost.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from takepicture import camera 3 | import os 4 | from azure.storage.blob import BlockBlobService, PublicAccess 5 | import json 6 | 7 | from iothub_client import IoTHubClient, IoTHubTransportProvider, IoTHubClientError 8 | from iothub_client import IoTHubMessage, IoTHubMessageDispositionResult 9 | from iothub_client import IoTHubClientRetryPolicy, IoTHubClientResult, IoTHubError 10 | 11 | app = Flask(__name__) 12 | 13 | TIMEOUT = 241000 14 | MINIMUM_POLLING_TIME = 9 15 | # messageTimeout - the maximum time in milliseconds until a message times out. 16 | # The timeout period starts at IoTHubClient.send_event_async. 17 | # By default, messages do not expire. 18 | MESSAGE_TIMEOUT = 10000 19 | # chose HTTP, AMQP, AMQP_WS or MQTT as transport protocol 20 | PROTOCOL = IoTHubTransportProvider.MQTT 21 | # used to pass as user context on Twin reporting 22 | TWIN_CONTEXT = 0 23 | 24 | # String containing Hostname, Device Id & Device Key in the format: 25 | # "HostName=;DeviceId=;SharedAccessKey=" 26 | try: 27 | CONNECTION_STRING = os.environ["CONNECTION_STRING"] 28 | except KeyError: 29 | pass 30 | 31 | try: 32 | account_name= os.environ["BLOB_ACCOUNT_NAME"] 33 | except KeyError: 34 | pass 35 | 36 | try: 37 | account_key= os.environ["BLOB_ACCOUNT_KEY"] 38 | except KeyError: 39 | pass 40 | 41 | 42 | def postblob(): 43 | blob_service = BlockBlobService(account_name, account_key) 44 | container_name = 'webcam' 45 | # in case you need to create the container 46 | # blob_service.create_container(container_name) 47 | # blob_service.set_container_acl(container_name, public_access=PublicAccess.Container) 48 | cam.TakePicture() 49 | blob_service.create_blob_from_path( 50 | container_name, 51 | 'picture', 52 | os.getcwd() + "/static/image.jpg" 53 | ) 54 | 55 | def receive_message_callback(message, counter): 56 | message_buffer = message.get_bytearray() 57 | size = len(message_buffer) 58 | msg = message_buffer[:size].decode('utf-8') 59 | if(msg == "picture"): 60 | postblob() 61 | return IoTHubMessageDispositionResult.ACCEPTED 62 | 63 | def device_twin_callback(update_state, payload, user_context): 64 | try: 65 | js = json.loads(payload) 66 | timezone = js["desired"]["timezone"] 67 | cam.timezone = int(timezone) 68 | reported_state = "{\"timezone\":" + str(cam.timezone) + "}" 69 | client.send_reported_state(reported_state, len(reported_state), send_reported_state_callback, TWIN_CONTEXT) 70 | except: 71 | pass 72 | 73 | def send_reported_state_callback(status_code, user_context): 74 | pass 75 | 76 | def iothub_client_init(): 77 | # prepare iothub client 78 | client = IoTHubClient(CONNECTION_STRING, PROTOCOL) 79 | if client.protocol == IoTHubTransportProvider.HTTP: 80 | client.set_option("timeout", TIMEOUT) 81 | client.set_option("MinimumPollingTime", MINIMUM_POLLING_TIME) 82 | # set the time until a message times out 83 | client.set_option("messageTimeout", MESSAGE_TIMEOUT) 84 | # to enable MQTT logging set to 1 85 | if client.protocol == IoTHubTransportProvider.MQTT: 86 | client.set_device_twin_callback( 87 | device_twin_callback, TWIN_CONTEXT) 88 | client.set_option("logtrace", 0) 89 | client.set_message_callback( 90 | receive_message_callback, 0) 91 | 92 | retryPolicy = IoTHubClientRetryPolicy.RETRY_INTERVAL 93 | retryInterval = 100 94 | client.set_retry_policy(retryPolicy, retryInterval) 95 | return client 96 | 97 | @app.route('/') 98 | def hello(): 99 | return "Hello from python flask webapp!, try /image.jpg /postimage /timezone" 100 | 101 | @app.route('/image.jpg') 102 | def image(): 103 | cam.TakePicture() 104 | return app.send_static_file('image.jpg') 105 | 106 | @app.route('/timezone') 107 | def timezone(): 108 | return "The timezone is: " + str(cam.timezone) + "h" 109 | 110 | @app.route('/postimage') 111 | def postimage(): 112 | postblob() 113 | return 'image posted https://portalvhdskb2vtjmyg3mg.blob.core.windows.net/webcam/picture' 114 | 115 | if __name__ == '__main__': 116 | # initialize the camera 117 | cam = camera() 118 | # initialize IoTHub 119 | client = iothub_client_init() 120 | # run flask, host = 0.0.0.0 needed to get access to it outside of the host 121 | app.run(host='0.0.0.0',port=1337) -------------------------------------------------------------------------------- /CameraIoTEdge/app/static/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/CameraIoTEdge/app/static/image.jpg -------------------------------------------------------------------------------- /CameraIoTEdge/app/takepicture.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import cv2 4 | import numpy as np 5 | import datetime 6 | 7 | class camera: 8 | "take a picture and saved it in the static folder" 9 | 10 | def __init__(self): 11 | # take the first camera 12 | self.cam = cv2.VideoCapture(0) 13 | self.timezone = 0 14 | pass 15 | 16 | def TakePicture(self): 17 | # read a frame 18 | ret, frame = self.cam.read() 19 | # where the code is running should be /app 20 | img_name = os.getcwd() + os.path.normpath("/static/image.jpg") 21 | _, width = frame.shape[:2] 22 | # Create a black image with same width as main image 23 | img = np.zeros((50,width,3), np.uint8) 24 | # Write the date 25 | font = cv2.FONT_HERSHEY_COMPLEX_SMALL 26 | bottomLeftCornerOfText = (10,25) 27 | fontScale = 1 28 | fontColor = (255,255,255) 29 | lineType = 1 30 | # format the datetime 31 | today = datetime.datetime.now() + datetime.timedelta(hours=self.timezone) 32 | thedate = '{:%Y/%m/%d %H:%M:%S}'.format(today) 33 | cv2.putText(img, thedate, 34 | bottomLeftCornerOfText, 35 | font, 36 | fontScale, 37 | fontColor, 38 | lineType) 39 | # Add both images and save the final image 40 | vis = np.concatenate((frame, img), axis=0) 41 | cv2.imwrite(img_name, vis) 42 | 43 | def __enter__(self): 44 | return self 45 | 46 | def __exit__(self, exc_type, exc_value, traceback): 47 | # close camera 48 | self.cam.release() 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/acrcheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/acrcheck.png -------------------------------------------------------------------------------- /docs/addmodule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/addmodule.png -------------------------------------------------------------------------------- /docs/azurecr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/azurecr.png -------------------------------------------------------------------------------- /docs/createedge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/createedge.png -------------------------------------------------------------------------------- /docs/dockerimages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/dockerimages.png -------------------------------------------------------------------------------- /docs/dockerps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/dockerps.png -------------------------------------------------------------------------------- /docs/moduledeployed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/moduledeployed.png -------------------------------------------------------------------------------- /docs/moduledetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/moduledetails.png -------------------------------------------------------------------------------- /docs/modulerunning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/modulerunning.png -------------------------------------------------------------------------------- /docs/setmodule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/setmodule.png -------------------------------------------------------------------------------- /docs/twin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellerbach/Raspberry-IoTEdge/9780436839b074b49a4b8b2b92d0490478849377/docs/twin.png -------------------------------------------------------------------------------- /opencv-330.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM resin/rpi-raspbian:stretch 2 | 3 | #Enforces cross-compilation through Quemu 4 | RUN [ "cross-build-start" ] 5 | 6 | # Install CV2 dependencies and build cv2 7 | RUN apt-get update && apt-get upgrade \ 8 | && apt-get install -y \ 9 | build-essential cmake pkg-config \ 10 | libjpeg-dev libtiff5-dev libjasper-dev libpng-dev \ 11 | libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \ 12 | libxvidcore-dev libx264-dev \ 13 | libgdk-pixbuf2.0-dev \ 14 | libfontconfig1-dev \ 15 | libcairo2-dev \ 16 | libpango1.0-dev \ 17 | libgdk-pixbuf2.0-dev \ 18 | libpango1.0-dev \ 19 | libxft-dev \ 20 | libfreetype6-dev \ 21 | libpng-dev \ 22 | libgtk2.0-dev \ 23 | libgtk-3-dev \ 24 | libatlas-base-dev gfortran \ 25 | python3-dev \ 26 | python3-pip \ 27 | wget \ 28 | unzip \ 29 | && rm -rf /var/lib/apt/lists/* \ 30 | && apt-get -y autoremove \ 31 | && wget -O opencv.zip https://github.com/opencv/opencv/archive/3.3.0.zip \ 32 | && unzip opencv.zip \ 33 | && rm -rf opencv.zip \ 34 | && wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/3.3.0.zip \ 35 | && unzip opencv_contrib.zip \ 36 | && rm -rf opencv_contrib.zip \ 37 | && pip3 install --upgrade pip \ 38 | && pip install numpy \ 39 | && ls \ 40 | && cd opencv-3.3.0/ \ 41 | && mkdir build \ 42 | && cd build \ 43 | && cmake -D CMAKE_BUILD_TYPE=RELEASE \ 44 | -D CMAKE_INSTALL_PREFIX=/usr/local \ 45 | -D INSTALL_PYTHON_EXAMPLES=ON \ 46 | -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-3.3.0/modules \ 47 | -D BUILD_EXAMPLES=ON .. \ 48 | && make -j4 \ 49 | && make install \ 50 | && ldconfig \ 51 | && rm -rf ../../opencv-3.3.0 ../../opencv_contrib-3.3.0 52 | 53 | RUN [ "cross-build-end" ] 54 | -------------------------------------------------------------------------------- /opencv-341.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM resin/rpi-raspbian:stretch 2 | 3 | #Enforces cross-compilation through Quemu 4 | RUN [ "cross-build-start" ] 5 | 6 | # Install CV2 dependencies and build cv2 7 | RUN apt-get update && apt-get upgrade \ 8 | && apt-get install -y \ 9 | build-essential cmake pkg-config \ 10 | libjpeg-dev libtiff5-dev libjasper-dev libpng-dev \ 11 | libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \ 12 | libxvidcore-dev libx264-dev \ 13 | libgdk-pixbuf2.0-dev \ 14 | libfontconfig1-dev \ 15 | libcairo2-dev \ 16 | libpango1.0-dev \ 17 | libgdk-pixbuf2.0-dev \ 18 | libpango1.0-dev \ 19 | libxft-dev \ 20 | libfreetype6-dev \ 21 | libpng-dev \ 22 | libgtk2.0-dev \ 23 | libgtk-3-dev \ 24 | libatlas-base-dev gfortran \ 25 | python3-dev \ 26 | python3-pip \ 27 | wget \ 28 | unzip \ 29 | && rm -rf /var/lib/apt/lists/* \ 30 | && apt-get -y autoremove \ 31 | && wget -O opencv.zip https://github.com/opencv/opencv/archive/3.4.1.zip \ 32 | && unzip opencv.zip \ 33 | && rm -rf opencv.zip \ 34 | && wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/3.4.1.zip \ 35 | && unzip opencv_contrib.zip \ 36 | && rm -rf opencv_contrib.zip \ 37 | && pip3 install --upgrade pip \ 38 | && pip install numpy \ 39 | && ls \ 40 | && cd opencv-3.4.1/ \ 41 | && mkdir build \ 42 | && cd build \ 43 | && cmake -D CMAKE_BUILD_TYPE=RELEASE \ 44 | -D CMAKE_INSTALL_PREFIX=/usr/local \ 45 | -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-3.4.1/modules \ 46 | -D BUILD_EXAMPLES=ON \ 47 | -D BUILD_WITH_DEBUG_INFO=OFF \ 48 | -D BUILD_DOCS=OFF \ 49 | -D BUILD_EXAMPLES=OFF \ 50 | -D BUILD_TESTS=OFF \ 51 | -D BUILD_opencv_ts=OFF \ 52 | -D BUILD_PERF_TESTS=OFF \ 53 | -D INSTALL_C_EXAMPLES=OFF \ 54 | -D INSTALL_PYTHON_EXAMPLES=ON \ 55 | -D ENABLE_NEON=ON \ 56 | -D WITH_LIBV4L=ON \ 57 | -D WITH_OPENGL=ON \ 58 | .. \ 59 | && make -j4 \ 60 | && make install \ 61 | && ldconfig \ 62 | && rm -rf ../../opencv-3.4.1 ../../opencv_contrib-3.4.1 63 | 64 | RUN [ "cross-build-end" ] 65 | -------------------------------------------------------------------------------- /opencv-342.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM resin/rpi-raspbian:stretch 2 | 3 | #Enforces cross-compilation through Quemu 4 | RUN [ "cross-build-start" ] 5 | 6 | # Install CV2 dependencies and build cv2 7 | RUN apt-get update && apt-get upgrade \ 8 | && apt-get install -y --no-install-recommends \ 9 | build-essential cmake pkg-config \ 10 | libjpeg-dev libtiff5-dev libjasper-dev libpng-dev \ 11 | libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \ 12 | libxvidcore-dev libx264-dev \ 13 | libgdk-pixbuf2.0-dev \ 14 | libfontconfig1-dev \ 15 | libcairo2-dev \ 16 | libpango1.0-dev \ 17 | libgdk-pixbuf2.0-dev \ 18 | libpango1.0-dev \ 19 | libxft-dev \ 20 | libfreetype6-dev \ 21 | libpng-dev \ 22 | libgtk2.0-dev \ 23 | libgtk-3-dev \ 24 | libatlas-base-dev gfortran \ 25 | python3-dev \ 26 | python3-pip \ 27 | wget \ 28 | unzip \ 29 | && rm -rf /var/lib/apt/lists/* \ 30 | && apt-get -y autoremove \ 31 | && wget -O opencv.zip https://github.com/opencv/opencv/archive/3.4.2.zip \ 32 | && unzip opencv.zip \ 33 | && rm -rf opencv.zip \ 34 | && wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/3.4.2.zip \ 35 | && unzip opencv_contrib.zip \ 36 | && rm -rf opencv_contrib.zip \ 37 | && pip3 install --upgrade pip \ 38 | && pip install numpy \ 39 | && cd opencv-3.4.2/ \ 40 | && mkdir build \ 41 | && cd build \ 42 | && cmake -D CMAKE_BUILD_TYPE=RELEASE \ 43 | -D CMAKE_INSTALL_PREFIX=/usr/local \ 44 | -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-3.4.2/modules \ 45 | -D BUILD_EXAMPLES=ON \ 46 | -D BUILD_WITH_DEBUG_INFO=OFF \ 47 | -D BUILD_DOCS=OFF \ 48 | -D BUILD_EXAMPLES=OFF \ 49 | -D BUILD_TESTS=OFF \ 50 | -D BUILD_opencv_ts=OFF \ 51 | -D BUILD_PERF_TESTS=OFF \ 52 | -D INSTALL_C_EXAMPLES=OFF \ 53 | -D INSTALL_PYTHON_EXAMPLES=ON \ 54 | .. \ 55 | # this is for building on a RPI to use all the cores 56 | # && make -j4 \ 57 | && make \ 58 | && make install \ 59 | && ldconfig \ 60 | && rm -rf ../../opencv-3.4.2 ../../opencv_contrib-3.4.2 61 | 62 | RUN [ "cross-build-end" ] 63 | 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Full example of Raspberry Pi with Camera using OpenCV Docker image, Python 3, Azure IoT Edge and Azure Blob Storage 2 | 3 | Some time ago, I've build a solution in my garden using a RaspberryPi (RPI), connected to Azure, sending temperature, wind speed, direction, air and soil humidifies. I did as well connect it to my own bot. So I iam able to ask my bot to send me a picture of my greenhouse just from Skype or Telegram for example. 4 | 5 | This project was done using NodeJS and the very recent at that time Azure IoT SDK for Node. Sources are [still available here](https://github.com/Ellerbach/nodejs-webcam-azure-iot). 6 | 7 | So very naturally, I decided to create the same project with the new Azure IoT Python SDK. And as there is now the opportunity to have IoT Edge devices and benefit of easy deployment of code thru containers, I've decided to do both at the same time. 8 | 9 | What are the challenges in this project? 10 | 11 | * There is a need of building a container for the RPI containing Python 3.5 and OpenCV to access to the webcam 12 | * Building an app in Python, connecting to Azure IoT Edge, listening to messages and posting the image to a blob storage. Using the concept of Device Twin in Azure Iot Hub to setup the device with specific settings 13 | * Setting up the RPI for Azure IoT Edge, setting up the parameters 14 | 15 | So let's start with the code part. 16 | 17 | ## Building the Python app, getting messages from Azure IoT hub, taking a picture and uploading it to Azure Blob 18 | 19 | The Python app is quite simple. On one side I have a class taking pictures and saving pictures. And one part connecting to Azure IoT Hub and publishing into an Azure Blog Storage. 20 | 21 | ### The Camera capture class 22 | 23 | This class does only contains one exposed function to take and save a picture. The other classes are internal ones. 24 | 25 | Initializing the camera is done thru ```cv2.VideoCapture(0)```. 0 is for the first video device. As there is just one webcam plugged, it's the one which will be used. 26 | 27 | ```PY 28 | import os 29 | import cv2 30 | import numpy as np 31 | import datetime 32 | 33 | class camera: 34 | "take a picture and saved it in the static folder" 35 | 36 | def __init__(self): 37 | # take the first camera 38 | self.cam = cv2.VideoCapture(0) 39 | self.timezone = 0 40 | pass 41 | ``` 42 | 43 | Take the picture is done thru ```ret, frame = self.cam.read()```. It does return a frame. The rest of the code is about adding a time stamp to the image. It create a sub image with have the same width as the main image, then write the date and time, then concatenate both image vertically, so the datetime will be under the picture and finally save the image. 44 | 45 | ```PY 46 | def TakePicture(self): 47 | # read a frame 48 | ret, frame = self.cam.read() 49 | # where the code is running should be /app 50 | img_name = os.getcwd() + os.path.normpath("/static/image.jpg") 51 | _, width = frame.shape[:2] 52 | # Create a black image with same width as main image 53 | img = np.zeros((50,width,3), np.uint8) 54 | # Write the date 55 | font = cv2.FONT_HERSHEY_COMPLEX_SMALL 56 | bottomLeftCornerOfText = (10,25) 57 | fontScale = 1 58 | fontColor = (255,255,255) 59 | lineType = 1 60 | # format the datetime 61 | today = datetime.datetime.now() + datetime.timedelta(hours=self.timezone) 62 | thedate = '{:%Y/%m/%d %H:%M:%S}'.format(today) 63 | cv2.putText(img, thedate, 64 | bottomLeftCornerOfText, 65 | font, 66 | fontScale, 67 | fontColor, 68 | lineType) 69 | # Add both images and save the final image 70 | vis = np.concatenate((frame, img), axis=0) 71 | cv2.imwrite(img_name, vis) 72 | ``` 73 | 74 | Something important to note is that the image is saved into a specific path. Flask uses a ```static``` folder to serve all static content. And the image will be saved before being served by the web server. So it has to store in this specific directory. The function ```os.getcwd()``` provide the working directory. As you can see in the container, it is the ```/app``` directory which contains both the py files as well as the static folder and an example of image. 75 | 76 | The ```__exist__``` function is just here to clean and close properly the camera once the Python code finishes. It is normally done when the Python session finished all up, but it's always better to leave things cleaned :-) 77 | 78 | ```PY 79 | def __enter__(self): 80 | return self 81 | 82 | def __exit__(self, exc_type, exc_value, traceback): 83 | # close camera 84 | self.cam.release() 85 | ``` 86 | 87 | ### The web server using flask 88 | 89 | I've decided to add a web server in the container to be able to easily test it. And I have as well another service at home which saves pictures every few minutes as well as allow me to receive them by mail time to time. Yes, I love to see how my cucumbers and salads are growing :-) 90 | 91 | Flask is very easy to use. There are still couple of tricks to know. 92 | 93 | ```PY 94 | from flask import Flask 95 | from takepicture import camera 96 | 97 | app = Flask(__name__) 98 | 99 | @app.route('/') 100 | def hello(): 101 | return "Hello from python flask webapp!, try /image.jpg /postimage" 102 | 103 | @app.route('/image.jpg') 104 | def image(): 105 | cam.TakePicture() 106 | return app.send_static_file('image.jpg') 107 | 108 | @app.route('/postimage') 109 | def postimage(): 110 | postblob() 111 | return "image posted https://portalvhdskb2vtjmyg3mg.blob.core.windows.net/webcam/picture" 112 | 113 | if __name__ == '__main__': 114 | # initialize the camera 115 | cam = camera() 116 | # initialize IoTHub 117 | client = iothub_client_init() 118 | # run flask, host = 0.0.0.0 needed to get access to it outside of the host 119 | app.run(host='0.0.0.0',port=1337) 120 | ``` 121 | 122 | This is a very basic example of Flask. The / path return just some text. The /image.jpg returns the actual image. It does illustrate the ```send_static_file('image.jpg')``` function which returns an actual file stored into the ```/static``` folder. That's the reason why the image is created there. 123 | 124 | /postimage just call the same function as when a message is received from Azure IoT Hub by the device. And return a text. 125 | 126 | In the ```__name__``` part, the camera is initialized, Azure IoT Hub as well. And finally Flask as well. 127 | 128 | Flask tip 1: use ```port=``` to run the web server on a specific port 129 | 130 | Flask tip 2 and **most important**: use ```host='0.0.0.0'``` if you want your web server to be accessible by any device. You can restrict the IP range if you want to let for example a subnet accessing. If you omit this, by default, only the machine running the code will have access. So in case you forget, only the code from the container will have access! 131 | 132 | ### Posting the image in the Azure Blob Storage 133 | 134 | If you don't have an Azure Blob Storage yet, just go to the [docs](https://docs.microsoft.com/en-us/azure/storage/) and create one. In few minutes, you'll be ready! 135 | 136 | Thanks to the Python SDK, it is extremly easy to post anything in a Blob storage. Authentication to the blob storage is as easy as ```blob_service = BlockBlobService(account_name, account_key)```. And posting as easy as ```blob_service.create_blob_from_path(container_name, 'picture', os.getcwd() + "/static/image.jpg")```. 137 | 138 | ```PY 139 | from azure.storage.blob import BlockBlobService, PublicAccess 140 | 141 | try: 142 | account_name= os.environ["BLOB_ACCOUNT_NAME"] 143 | except KeyError: 144 | pass 145 | 146 | try: 147 | account_key= os.environ["BLOB_ACCOUNT_KEY"] 148 | except KeyError: 149 | pass 150 | 151 | def postblob(): 152 | blob_service = BlockBlobService(account_name, account_key) 153 | container_name = 'webcam' 154 | # in case you need to create the container 155 | # blob_service.create_container(container_name) 156 | # blob_service.set_container_acl(container_name, public_access=PublicAccess.Container) 157 | cam.TakePicture() 158 | blob_service.create_blob_from_path( 159 | container_name, 160 | 'picture', 161 | os.getcwd() + "/static/image.jpg" 162 | ) 163 | ``` 164 | 165 | You'll not the ```os.environ["BLOB_ACCOUNT_NAME"]``` in a try except bloc. This is the way to test if an environement variable exist and if it does exist, save it into both variables used to access the blob storage. 166 | 167 | It is very easy to set environment variables in Azure IoT Edge, we will see later how to do it. 168 | 169 | If the container is not created or you want to make sure it is, with the right access rights, use The 2 more commented lines. Keep in mind the access of the container looks like: ```https://portalvhdskb2vtjmyg3mg.blob.core.windows.net/webcam/picture``` where: 170 | 171 | * ```portalvhdskb2vtjmyg3mg``` is the account name 172 | * ```webcam``` is the container name 173 | * ```picture``` is the object name. It can be even ```image.jpg``` or any other name. 174 | 175 | ### Azure IoT Hub connection and receiving messages 176 | 177 | To connect to Azure IoT Hub, you'll need to create an Azure IoT Hub. As always, in few minutes following the [docs](https://docs.microsoft.com/en-us/azure/iot-hub/), you'll be ready to go. 178 | 179 | Once you'll have your hub created, using the same docs, create an IoTEdge device. You'll need to get the connection string for later on. 180 | 181 | ```PY 182 | from iothub_client import IoTHubClient, IoTHubTransportProvider 183 | from iothub_client import IoTHubMessage, IoTHubMessageDispositionResult 184 | from iothub_client import IoTHubClientRetryPolicy 185 | 186 | TIMEOUT = 241000 187 | MINIMUM_POLLING_TIME = 9 188 | # messageTimeout - the maximum time in milliseconds until a message times out. 189 | # The timeout period starts at IoTHubClient.send_event_async. 190 | # By default, messages do not expire. 191 | MESSAGE_TIMEOUT = 10000 192 | # chose HTTP, AMQP, AMQP_WS or MQTT as transport protocol 193 | PROTOCOL = IoTHubTransportProvider.MQTT 194 | 195 | # String containing Hostname, Device Id & Device Key in the format: 196 | # "HostName=;DeviceId=;SharedAccessKey=" 197 | try: 198 | CONNECTION_STRING = os.environ["CONNECTION_STRING"] 199 | except KeyError: 200 | pass 201 | 202 | def receive_message_callback(message, counter): 203 | message_buffer = message.get_bytearray() 204 | size = len(message_buffer) 205 | msg = message_buffer[:size].decode('utf-8') 206 | if(msg == "image"): 207 | postblob() 208 | return IoTHubMessageDispositionResult.ACCEPTED 209 | 210 | def iothub_client_init(): 211 | # prepare iothub client 212 | client = IoTHubClient(CONNECTION_STRING, PROTOCOL) 213 | if client.protocol == IoTHubTransportProvider.HTTP: 214 | client.set_option("timeout", TIMEOUT) 215 | client.set_option("MinimumPollingTime", MINIMUM_POLLING_TIME) 216 | # set the time until a message times out 217 | client.set_option("messageTimeout", MESSAGE_TIMEOUT) 218 | # to enable MQTT logging set to 1 219 | if client.protocol == IoTHubTransportProvider.MQTT: 220 | client.set_option("logtrace", 0) 221 | client.set_message_callback( 222 | receive_message_callback, 0) 223 | 224 | retryPolicy = IoTHubClientRetryPolicy.RETRY_INTERVAL 225 | retryInterval = 100 226 | client.set_retry_policy(retryPolicy, retryInterval) 227 | return client 228 | 229 | ``` 230 | 231 | The most complicated part if the ```iothub_client_init()```. It does prepare all what is needed to have the authentication with Azure IoT Hub as well as setting up the call back methods. I've let the choice between using HTTP and MQTT in this code. I do recommend MQTT. But it shows you that the way to handle the connections between those 2 protocols is slightly different. By essence, HTTP is a pure disconnected protocol while MQTT is not. 232 | 233 | The very interesting part is the ```receive_message_callback(message, counter)``` function. Once a message is sent to the Azure IoT Hub for the device, this function will be called. The message is encoded and need to be decoded. 234 | 235 | The code is very straight forward, it just decode, check if the message is "image" and if true, call the function to take a picture and upload it to the blob storage. 236 | 237 | This code is not posting any data on Azure IoT Hub. It can of course. But it's done in another part of the code, I did not rewrite yet. And it can be anyway in another container. This allow to separate various function and feature and allowing to deploy updates for only parts of the code. 238 | 239 | ### Using Device Twin to pass settings to the device 240 | 241 | Device Twin is a great concept in Azure IoT Hub. It does allow to specify specific settings for the device and as well to the device to push some specific states. I will use it to push the time zone to use for the picture which will be taken by the camera. The code will run in a docker container and by default, it uses the UTC time without any time zone. One option is to map the time from the container. But because I want to illustrate the concept of device twin, I won't use it. 242 | 243 | There is a bit of code to add from the previous part. It's just about adding 2 functions. One which is a callback function called when there is a configuration to pass, one to call (it's technically a call back as well) when you send a configuration. 244 | 245 | ```python 246 | def device_twin_callback(update_state, payload, user_context): 247 | try: 248 | js = json.loads(payload) 249 | timezone = js["desired"]["timezone"] 250 | cam.timezone = int(timezone) 251 | reported_state = "{\"timezone\":" + str(cam.timezone) + "}" 252 | client.send_reported_state(reported_state, len(reported_state), send_reported_state_callback, TWIN_CONTEXT) 253 | except: 254 | pass 255 | 256 | def send_reported_state_callback(status_code, user_context): 257 | pass 258 | ``` 259 | 260 | The ```device_twin_callback``` function is called when a new configuration is sent. In our case, we will setup the device twin to send a timezone parameter as an integer. It will be a shift time for the camera timestamp. What is send to the device is a simple json containing the full configation of both what is desired and last reported. 261 | 262 | ```json 263 | {'desired': {'timezone': 2, '$version': 4}, 'reported': {'timezone': 2, '$version': 2}} 264 | ``` 265 | 266 | So we just analyze the json, get the *desired* node then the *timezone* element and convert it to int and pass it to the camera. Once done, to make sure we report the state, we just send back the configuration to the IoT Hub. 267 | 268 | To setup the device twin in Azure, from the portal, just go on your device, add the timezone in the desired section and save. 269 | 270 | ![twin setup](/docs/twin.png) 271 | 272 | The portal will give you informations of latestest update. You'll note from what is sent to the device and wha tis reported to the portal a version number. It is automatically incremented everytime you modify the twin. It's a way to keep track of modication and allow you to manage it in your code as well. You'll notice as well the context which is passed, it's as well a way for you to add some information. 273 | 274 | And of course, you need to initialize all this in the ```iothub_client_init()``` function. Just add the line to say we want a call back. 275 | 276 | ```Python 277 | def iothub_client_init(): 278 | # ... 279 | # code before 280 | if client.protocol == IoTHubTransportProvider.MQTT: 281 | client.set_device_twin_callback( 282 | device_twin_callback, TWIN_CONTEXT) 283 | client.set_option("logtrace", 0) 284 | # code after 285 | # ... 286 | ``` 287 | 288 | To test if the setting is passed correctly, we can use a simple Flask route: 289 | 290 | ```python 291 | @app.route('/timezone') 292 | def timezone(): 293 | return "The timezone is: " + str(cam.timezone) + "h" 294 | ``` 295 | 296 | so you can easilly test if the twin configuration has been passed. And of course, you can test it directly in the picture. 297 | 298 | ## Building a Docker OpenCV image with Python 3.5 for RaspberryPi and other armhf devices 299 | 300 | On armhf for the Raspberry, there is no pre made PyPi (pip) install. So in short, we'll have to fully build an image for the RPI. OpenCV is a very powerful library but it's a bit one with tons of dependencies. 301 | One good source to learn how to recompile correctly OpenCV is to use the site [pyimagesearch](https://www.pyimagesearch.com/). I've been using it as a first source as it does give a good view of dependencies, what needs to be downloaded, the build settings. 302 | 303 | Once I understood the key principles, and did it once, I started to put everything at once in a docker image. You'll find 3 files in the project compiling various versions of OpenCv 3.3.0, 3.4.1 and 3.4.2. I've been adjusting a bit dependencies as well as what I wanted to build in the project. 304 | 305 | ### Creating the Docker image with only Python 3.5 and OpenCV 306 | 307 | To do this, we need to start with an existing image. resin has built various images of Raspberry Pi. We will use the later stretch image. We will as well make sure we are in a cross compilation environment. 308 | 309 | ```CMD 310 | FROM resin/rpi-raspbian:stretch 311 | 312 | #Enforces cross-compilation through Quemu 313 | RUN [ "cross-build-start" ] 314 | ``` 315 | 316 | Then we will add all the dependencies to properly build OpenCV, get python and pip. 317 | 318 | ```CMD 319 | RUN apt-get update && apt-get upgrade \ 320 | && apt-get install -y --no-install-recommends \ 321 | build-essential cmake pkg-config \ 322 | libjpeg-dev libtiff5-dev libjasper-dev libpng-dev \ 323 | libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \ 324 | libxvidcore-dev libx264-dev \ 325 | libgdk-pixbuf2.0-dev \ 326 | libfontconfig1-dev \ 327 | libcairo2-dev \ 328 | libpango1.0-dev \ 329 | libgdk-pixbuf2.0-dev \ 330 | libpango1.0-dev \ 331 | libxft-dev \ 332 | libfreetype6-dev \ 333 | libpng-dev \ 334 | libgtk2.0-dev \ libgtk-3-dev \ 335 | libatlas-base-dev gfortran \ 336 | python3-dev \ 337 | python3-pip \ 338 | wget \ 339 | unzip \ 340 | && rm -rf /var/lib/apt/lists/* \ 341 | && apt-get -y autoremove \ 342 | ``` 343 | 344 | Then download and extract the OpenCV source code 345 | 346 | ```CMD 347 | && wget -O opencv.zip https://github.com/opencv/opencv/archive/3.4.2.zip \ 348 | && unzip opencv.zip \ 349 | && rm -rf opencv.zip \ 350 | && wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/3.4.2.zip \ 351 | && unzip opencv_contrib.zip \ 352 | && rm -rf opencv_contrib.zip \ 353 | ``` 354 | 355 | Then we will make sure we'll have the last version of PyPi and install numpy 356 | 357 | ```CMD 358 | && pip3 install --upgrade pip \ 359 | && pip install numpy \ 360 | ``` 361 | 362 | And finally build OpenCV and clean the source code. 363 | 364 | ```CMD 365 | && cd opencv-3.4.2/ \ 366 | && mkdir build \ 367 | && cd build \ 368 | && cmake -D CMAKE_BUILD_TYPE=RELEASE \ 369 | -D CMAKE_INSTALL_PREFIX=/usr/local \ 370 | -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-3.4.2/modules \ 371 | -D BUILD_EXAMPLES=ON \ 372 | -D BUILD_WITH_DEBUG_INFO=OFF \ 373 | -D BUILD_DOCS=OFF \ 374 | -D BUILD_EXAMPLES=OFF \ 375 | -D BUILD_TESTS=OFF \ 376 | -D BUILD_opencv_ts=OFF \ 377 | -D BUILD_PERF_TESTS=OFF \ 378 | -D INSTALL_C_EXAMPLES=OFF \ 379 | -D INSTALL_PYTHON_EXAMPLES=ON \ 380 | .. \ 381 | # this is for building on a RPI to use all the cores 382 | # && make -j4 \ 383 | && make \ 384 | && make install \ 385 | && ldconfig \ 386 | && rm -rf ../../opencv-3.4.2 ../../opencv_contrib-3.4.2 387 | 388 | RUN [ "cross-build-end" ] 389 | ``` 390 | 391 | This will allow to build the container. You can build it on a Raspberry Pi or on a Windows machine with Docker installed. I've tried both method and it is much faster on a Windows machine. For this, you just need to make sure you have Docker running Linux containers. 392 | 393 | ```CMD 394 | docker build -t ellerbach/opencv:342 -f opencv-342.Dockerfile . 395 | ``` 396 | 397 | Be patient, it will take a while, couple of hours. Full size of the image is bit less than 1Gb. 398 | 399 | #### Build done correctly 400 | 401 | Yea! celebrate, all good, you made it! 402 | 403 | #### Something went wrong 404 | 405 | It can happen. This can be due to multiple factors, so check them carefully: 406 | 407 | * Check the error message, it can be due to a missing library. So just change the library, add/remove it from the list 408 | * It can be due to an incompatibility version between 2 libraries. It happened a lot to me with similar projects. Try to find the problem, remove the dependencies and try to add remove some of the libraries up to the point you won't get any issue 409 | * A library is missing and you have the message during the OpenCV compilation, so just add it to the dependency list 410 | 411 | Tip: slice the problem, create intermediate images if needed. I explained the principle in [this repository](https://github.com/Ellerbach/Raspberry-Docker-Tensorflow-Pillow-Flask). 412 | 413 | ### Creating the image for the final application 414 | 415 | The final application is a Python app using Azure IoT Edge so it will require the [Azure IoT Python SDK](https://github.com/Azure/azure-iot-sdk-python) as well as Azure Blog Storage so it will require [Azure Storage Python SDK](https://github.com/Azure/azure-storage-python). Installation is quite straight forward, don't forget to add libboost-python-dev as Azure IoT Hub is using it. I'm using as well flask as you can see from the code section. The dockerfile for the app is available [here](./CameraIoTEdge/Dockerfile). This Docker file is then much simpler than the previous one: 416 | 417 | ```CMD 418 | FROM ellerbach/opencv:342 419 | 420 | ADD app app 421 | 422 | RUN apt-get update && apt-get upgrade \ 423 | && apt-get install libboost-python-dev \ 424 | && pip3 install azure-iothub-device-client \ 425 | && pip3 install azure-storage-blob \ 426 | && pip3 install flask \ 427 | && pip3 install numpy \ 428 | && sudo rm -rf /var/lib/apt/lists/* 429 | 430 | # Set the working directory 431 | WORKDIR /app 432 | 433 | EXPOSE 1337 434 | 435 | # Run the flask server for the endpoints 436 | CMD ["python3","AzurePost.py"] 437 | ``` 438 | 439 | To build the image we will have to do a traditional: 440 | 441 | ```CMD 442 | docker build -t ellerbach/pythontest:342 . 443 | ``` 444 | 445 | This will take couple of minutes, at this stage, all should be good and working. 446 | 447 | ### Pushing the docker image to the Azure Container Registry (ACR) 448 | 449 | Once you're created you image, you'll need to push it to your own Azure Container Registry. By default your registry is private allowing to store images in a secure way, deploying them in a secure way as well. 450 | 451 | #### Step 1: Create your Azure Container Registry 452 | 453 | Takes only few clicks when you have an Azure subscription. All explained [here](https://docs.microsoft.com/en-us/azure/container-registry/). Use your preferred method, either CLI, either Portal, either PowerShell :-) 454 | 455 | #### Step 2: log to the ACR 456 | 457 | From a command line where you have build the previous image, log in: 458 | 459 | ```CMD 460 | docker login myregistry.azurecr.io -u yourusername -p yourpassword 461 | ``` 462 | 463 | where: 464 | 465 | * ```myregistry``` is your registry name 466 | * ```yourusername``` is your user name 467 | * ```yourpassword``` is your password 468 | 469 | You'll find all of them easilly in the portal. As for example from the documentation: 470 | ![Azure portal](https://docs.microsoft.com/en-us/azure/container-registry/media/container-registry-get-started-portal/qs-portal-06.png) 471 | 472 | #### Step 3: tag your image for ACR 473 | 474 | In order to be able to push the image, it needs to be tagged. In my case: 475 | 476 | ```CMD 477 | docker tag ellerbach/pythontest:342 ellerbach.azurecr.io/ellerbach/pythontest:342 478 | ``` 479 | 480 | I'm using the previous built image and tag it for my Azure CR. You can use a short name, no need to add sub directories. I did it just for the example. 481 | 482 | #### Step 4: push your image 483 | 484 | The classical push will make it. Just keep in mind you're pushing it to your Azure Container Registry. So you have to be logged. 485 | 486 | ```CMD 487 | docker push ellerbach.azurecr.io/ellerbach/pythontest:342 488 | ``` 489 | 490 | Depending on your available bandwidth, it may take couple of minutes. 491 | 492 | #### Step 5: check all is ok 493 | 494 | Go to the Azure portal, connect, go to your created directory and check if your image is present: 495 | 496 | ![Azure portal](./docs/acrcheck.png) 497 | 498 | In my case, I can find the one tagged 342. 499 | 500 | ## Deploying Azure IoT Edge on the device and setting up the Azure IoT Edge on the server saide 501 | 502 | Now we've created our code, our container, the last step is to setup Azure IoT Edge. I will show how to do it in the portal. It is as well possible using CLI or the VS Code extension. 503 | 504 | ### Create an Azure IoT Hub 505 | 506 | To connect to Azure IoT Hub, you'll need to create an Azure IoT Hub. As always, in few minutes following the [docs](https://docs.microsoft.com/en-us/azure/iot-hub/), you'll be ready to go. 507 | 508 | ### Create an Azure IoT Edge device 509 | 510 | From your newly created Azure IoT Edge, you can now create an IoT Edge device. Click on IoT Edge, then Add IoT Edge, fill the name, click add and you'll see your device in the device list like in the picture below: 511 | 512 | ![Create IoT Edge device](./docs/createedge.png) 513 | 514 | ### Installing Azure IoT Edge runtime on the device 515 | 516 | To install the runtime on your Raspberry, [follow the docs](https://docs.microsoft.com/en-us/azure/iot-edge/how-to-install-iot-edge-linux-arm). It is very well done, step by steps and as well all you need to troubleshoot if you have any issue. 517 | 518 | Keep in mind your RPI is an armhf architecture, so follow the right path of installation. Keep in mind, so far, only Raspbian Stretch (so based on Debian 9) version is supported. If for some reason, you can't migrate to Stretch and you are still on Jessie (so Debian 8), then [follow my tutorial here](https://blogs.msdn.microsoft.com/laurelle/2018/08/17/azure-iot-edge-support-for-raspbian-8-0-debian-8-0/) and you'll still be able to enjoy Azure IoT Edge. But please note it is an unsupported scenario and it may not work. 519 | 520 | ### Setting up the edge container 521 | 522 | I'll use the portal to show to do it. It's perfectly ok if you have only 1 device. If you have more, I recommend to use ARM templates and use VS Code Azure IoT Edge extension for example. 523 | 524 | First, open your device by clicking on it. Then click on ```Set modules``` 525 | 526 | ![set modules](./docs/setmodule.png) 527 | 528 | Then click on ```Add``` and then ```IoT Edge Module``` 529 | 530 | ![add modul](./docs/addmodule.png) 531 | 532 | Now you'll have to fill the form. 533 | 534 | ![module details](./docs/moduledetails.png) 535 | 536 | Pay attention to few important things: 537 | 538 | * ```Name```is a name, the one you want, makes it easy then to find a module 539 | * ```Image URI``` is the full URI for the container. In my case, see from the previous part, it is ellerbach.azurecr.io/ellerbach/pythontest:342. And keep in mind you'll have to specify the tag as well. 540 | * ```Container Create Options``` is one of the most important part as it has to contain all the options you'll put if you'll have to run the container manually 541 | 542 | ```JSON 543 | { 544 | "ExposedPorts": { 545 | "1337/tcp": {} 546 | }, 547 | "HostConfig": { 548 | "Privileged": true, 549 | "PortBindings":{ 550 | "1337/tcp": [ 551 | { 552 | "HostPort": "1337" 553 | } 554 | ] 555 | } 556 | } 557 | } 558 | ``` 559 | 560 | In our case, important things to notice: 561 | 562 | * we are exposing a port out of the container and want all outside machines to be able to access it. This is done with the ```ExposedPorts``` and the ```PortBindings``` section. 563 | * ```Priviledge``` is a very important one. It will give access to the container to everything from the host. In our case, it is needed as the webcam needs to access the /dev/video0 port. By default, there is no access granted at all! So don't forget this one 564 | 565 | * The environment variables need to be set as well. They are used to connect to the Azure IoT Hub message callback as well as posting the picture to the blob storage 566 | * Don't forget to hit save, then next then submit and you'll be good to go 567 | 568 | You should now see your container appearing in the list: 569 | 570 | ![module deployed](./docs/moduledeployed.png) 571 | 572 | And if all goes right, after some time, you'll see the module deployed and running: 573 | 574 | ![module running](./docs/modulerunning.png) 575 | 576 | **Warning:** the deployment of large containers can takes hours! My container is about 1Gb, so be very patient! Don't rush, don't change anything on the device, just wait. 577 | 578 | ### Troubleshooting 579 | 580 | Typically few things can happen. Let's see what we can do to check. 581 | 582 | #### You've waited enough and the container is not deploying 583 | 584 | You can force the deployment from the device and check if it is deploying. First check if it is present. 585 | 586 | ```CMD 587 | docker images 588 | ``` 589 | 590 | If it is present, you should see it in the list: 591 | 592 | ![docker images](./docs/dockerimages.png) 593 | 594 | If not, then try to manually download it on the device. You will see if any problem happen. 595 | 596 | ```CMD 597 | docker login myregistry.azurecr.io -u yourusername -p yourpassword 598 | docker pull myregistry.azurecr.io/mycontainer:tag 599 | ``` 600 | 601 | where: 602 | 603 | * ```myregistry``` is your registry name 604 | * ```yourusername``` is your user name 605 | * ```yourpassword``` is your password 606 | * ```mycontainer``` is your container name 607 | * ```tag``` is the tag 608 | 609 | You should see the download and then see the image in the list. 610 | 611 | ### Your container is present but not running 612 | 613 | First try to check if the container is running or not: 614 | 615 | ```CMD 616 | docker ps 617 | ``` 618 | 619 | ![docker ps](./docs/dockerps.png) 620 | 621 | if the container is not running but is present, try to restart the iotedge engine. 622 | 623 | ```CMD 624 | sudo systemctl restart iotedge 625 | ``` 626 | 627 | If you still don't see it, please have a look at other deep way to debug all this in the [Azure IoT Edge docs](https://docs.microsoft.com/en-us/azure/iot-edge/troubleshoot#stability-issues-on-resource-constrained-devices). 628 | 629 | ### Your container is deployed, run but failed 630 | 631 | This is an interesting case as it means the code in the container is crashing the container. You'll have to debug it. For this, one of the best way is to attach docker to the container and look at the logs. 632 | 633 | ```CMD 634 | docker ps 635 | docker attached CONTAINER_ID 636 | ``` 637 | 638 | where ```CONTAINER_ID``` is the container ID displayed when running the ```docker ps``` command. In the previous example, it will be 72dea57b921c. 639 | 640 | You can as well run the container out of IoT Edge and debug as well. In my case, the command will be: 641 | 642 | ```CMD 643 | docker login myregistry.azurecr.io -u yourusername -p yourpassword 644 | docker run -p 0.0.0.0:1337:1337 --privileged -e CONNECTION_STRING='CONNECTION_STRING' -e BLOB_ACCOUNT_NAME='BLOB_ACCOUNT_NAME' -e BLOB_ACCOUNT_KEY='BLOB_ACCOUNT_KEY' 645 | ellerbach.azurecr.io/ellerbach/pythontest:342 646 | ``` 647 | 648 | where: 649 | 650 | * you need to run the first command if you're not already connected 651 | * ```myregistry``` is your registry name 652 | * ```yourusername``` is your user name 653 | * ```yourpassword``` is your password 654 | * Replace the connection string, blob account and blob key with the correct values. The environment variables are required to run correctly. If you don't add them, the code will break. 655 | 656 | From there, you'll be able to debug, place any trace in your main container and understand what is wrong. 657 | 658 | ## Conclusion 659 | 660 | This example is complete and shows how to create a complex container, with compiling some modules, adding custom code, connecting to Azure IoT Hub as well as Azure Blob Storage. 661 | 662 | Note: after finishing to write the code, deploy the code on my device, I found a more complete example. You'll be able to access it [from here](https://azure.microsoft.com/en-us/resources/samples/custom-vision-service-iot-edge-raspberry-pi/). The philosophy is the same. So enjoy the example as well! 663 | --------------------------------------------------------------------------------