Gustav Kånåhols
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    1
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    *Created and written by Gustav Kånåhols (gk222kj).* **Table of Contents** [TOC] I've created a system which gathers and stores data about a cat litter box and reminds its owner (me) to clean the box when needed. The system consists of an IoT device with sensors which attaches to the box, hereafter refered to as the Box Unit (BU), and a server running a number of services including a MQTT-broker. The project will take anywhere from 4-24+ hours depending on your experience with docker and Linux in general as well as on whether you decide to 3D-print the box and solder the final circuit or not. Code, schematics and docker files can be found in the [GitHub repository](https://github.com/Kurbitz/litter-box-detector) ### Objective I chose this project because I wanted to create something to help me with a chore I don't like doing. The data gathered by the system will be used to remind me to clean the box when needed and to keep track of how often I clean it. I want to use the data in order to get a better understanding of the cat's behavior and how it affects the frequency of cleaning. I also want to see if there are any patterns in the data that can be used to predict when the box needs to be cleaned. One of my goals was to host everything locally, meaning I wouldn't have to rely on any third-party services. I also wanted to use as much open-source software as possible. This would allow me to customize the system to my liking and to add new features if I wanted to. I would also be able to host the system on my LAN wihtout transmitting any data over the internet. This would allow me to keep the data private and to avoid any potential security issues. ### Material Below is a list of all the components used in the project. The prices are in SEK. Items marked with "*" are optional. | Component | Price | Link| | -------- | -------- | -------- | | NodeMCU ESP8266 | 149.00 | [ElectroKit](https://www.electrokit.com/produkt/nodemcu-v3/) | | PIR-sensor | 49.00 | [ElectroKit](https://www.electrokit.com/produkt/pir-rorelsedetektor-hc-sr501/) | | DHT11 | 49.00 | [ElectroKit](https://www.electrokit.com/produkt/digital-temperatur-och-fuktsensor-dht11/) | | *LCD 1602 display (any HD44780-compatible display will work) | 99.00 | [ElectroKit](https://www.electrokit.com/produkt/lcd-2x16-tecken-jhd162a-stn-bla-vit-led/) | | *I2C LCD backpack | 49.00 | [ElectroKit](https://www.electrokit.com/produkt/i2c-interface-for-lcd/) | | *Button (any push-button will work) | 69.90 | [Kjell & Company](https://www.kjell.com/se/produkter/el-verktyg/elektronik/elektromekanik/strombrytare-for-elektronik/tryckstrombrytare/strombrytare-1-pol-frantill-p36134) | In addition to the components listed above you'll also need a breadboard, a bunch of jumper wires and a USB-cable to power the NodeMCU. I chose not to include these in the list since they're not really components. The breadboard and jumper wires can be bought from ElectroKit. The USB-cable can be bought from pretty much anywhere. I used a USB-cable from an old phone charger. I chose to solder the final circuit together on a perfboard but you could just as well use a breadboard. The perfboard and soldering equipment can be bought from basically any electronics store (like ElectroKit) or borrowed at a local makerspace. #### Microcontroller The microcontroller used is a NodeMCU ESP8266. It was chosen over the Pico (which was recommended by the course) because it's far more popular and has a lot more support. I also had a couple of them lying around from previous projects. The NodeMCU is a development board with a ESP8266 microcontroller, a USB port for power and programming, and a bunch of GPIO pins. It's also got a built-in WiFi module which is what I'll be using to send data to the server. ![](https://i.imgur.com/B7FEXUm.jpg =500x) #### Sensors The sensors used are a DHT11 and a PIR-sensor. ##### DHT11 The DHT11 is a temperature and humidity sensor. It's not very accurate but it's cheap and easy to use. ![](https://i.imgur.com/XeiU8xs.jpg =400x) ##### PIR-sensor The PIR-sensor is a motion sensor which is used to detect when the cat is in the box. It's also quite cheap and readily available. ![](https://i.imgur.com/qgbTDfC.jpg =400x) #### Input and output In addition to the sensors I added a button, LCD-display and a status-LED. All of this is not strictly necessary but it makes the system a lot more stylish and user-friendly. One could make do with just the sensors and the microcontroller and control the system through something like a web interface instead. But overkill is the best kind of kill. ##### Button The button is a generic panel-mount push-button. The size and shape doesn't really matter as long as it fits in the box. Modifications to the box might be necessary depending on the button you choose. The button is used to register when the box is cleaned. ![](https://i.imgur.com/UtMr9Ze.png =200x200) ##### LCD-display The LCD is a 1602 display with a HD44780-compatible controller. It's attached to the NodeMCU using a I2C LCD backpack which makes it a lot easier to wire up and use. Again, modifications to the box might be necessary depending on the display you choose. ![](https://i.imgur.com/QuWDt0n.jpg =400x) ![](https://i.imgur.com/hAFIsXy.jpg =400x) ##### Case The box unit was assembled using a 3D-printed case. The case was designed in Fusion 360 and printed on a Prusa mini. The STL and STEP-files can be found in the [GitHub repository](https://https://github.com/Kurbitz/litter-box-detector/tree/main/models). The case consists of two parts, the bottom and the lid. The bottom part has four standoffs for mounting the PCB and lugs in the corners for mounting the case to the litter box. The lid has slots for the LCD, button and status-LED. The lid is attached to the bottom using four M3 screws. The case was printed using PLA with 20% infill and 0.2mm layer height. The print took about 4 hours to complete. If you don't own a 3D-printer you can probably find someone who does at a local makerspace. Alternatively you could just use a cardboard box or something similar. Rendering of the case: Top part ![](https://i.imgur.com/uBWZvG5.png) Bottom part ![](https://i.imgur.com/gp8Wcuo.png) ### Computer setup To set up the development environment you'll need to install a few dependencies. I will give a brief overview of the steps needed to set up the environment on Linux. If you're using Windows or MacOS some of the steps will be different. I will not go into detail on how to set up the environment on those platforms but I will try to provide links to the proper documentation. When finished you should have the following installed: - Python 3 - VSCode - *(The IDE used to write code)* - PyMakr plugin for VSCode - *(Used to upload code to the NodeMCU)* - Node.js - *(Used by the PyMakr plugin)* - esptool - *(Used to flash the NodeMCU)* You will also need to download the MicroPython firmware for the NodeMCU. You can download it from [here](https://micropython.org/download/esp8266/). Make sure to download the latest stable version. The firmware is a binary file which you will need to flash to the NodeMCU. More on that later. Note that if you want linting and code completion you'll also need a few more things. I will not go into detail on how to set that up but you can find information about it [here](https://micropython-stubber.readthedocs.io/en/latest/intro.html). #### Python 3 The first thing you'll need is Python 3. If you're using Linux you probably already have it installed. If not you can install it using your package manager. On Ubuntu you can install it using the following command: ```sudo apt install python3``` #### VSCode The next thing you'll need is VSCode. You can download it from [here](https://code.visualstudio.com/). Once you've downloaded it you can install it by following the instructions on the download page. #### PyMakr plugin The PyMakr plugin is used to upload code to the NodeMCU. You can install it by opening VSCode and clicking on the extensions tab in the sidebar. Search for PyMakr and click install. Once it's installed you'll need to restart VSCode. #### Node.js To get the PyMakr plugin to work you'll need to install Node.js. On Ubuntu you can install it using the following command: ```sudo apt install nodejs``` #### esptool The last thing you'll need is esptool. It's used to flash the NodeMCU with MicroPython. You can install it from PyPI by running the following command: ```pip install esptool``` More information on how to install esptool can be found [here](https://docs.espressif.com/projects/esptool/en/latest/esp8266/installation.html) #### Flashing the NodeMCU Once you've installed all the dependencies you're ready to flash the NodeMCU with MicroPython. To do this you'll need to connect the NodeMCU to your computer using a USB-cable. Once it's connected you'll need to find out which port it's connected to. On Linux you can do this by running the following command: ```ls /dev/ttyUSB*``` The output should be something like ```/dev/ttyUSB0```. If you're using Windows or MacOS you can find instructions on how to find the port [here](https://docs.micropython.org/en/latest/esp8266/tutorial/intro.html#deploying-the-firmware). The port will probably be something like ```COM1``` on Windows and ```/dev/cu.usbserial-0001``` on MacOS. Now that you know which port the NodeMCU is connected to you can flash it with MicroPython. To do this you'll need to run the following command: ```esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=detect -fm dout 0 esp8266-20230426-v1.20.0.bin``` Replace ```/dev/ttyUSB0``` with the port you found earlier and ```esp8266-20230426-v1.20.0.bin``` with the name of the firmware file you downloaded from [micropython.org](https://micropython.org/download/esp8266/) earlier (make sure you're using the correct path). ### Putting everything together #### Overview The circuit consists of the NodeMCU, the DHT11, the PIR-sensor, the button, the status-LED and the LCD-display. The NodeMCU is connected to the DHT11, the PIR-sensor, the button and the status-LED using jumper wires. The LCD-display is connected to the NodeMCU using a I2C LCD backpack. The NodeMCU is powered using a USB-cable connected to a USB-charger. This is what the circuit looks like: ![](https://i.imgur.com/VEB1x16.png) It could all be conected like this on a breadboard: ![](https://i.imgur.com/Jd8w56T.png) You can find a Fritzing file of the circuit in the [GitHub repository](https://github.com/Kurbitz/litter-box-detector/blob/main/schematics/Litterbox.fzz). #### Resistor values The R1 resistor is used to regulate the current-draw of the LED. The NodeMCU's GPIO pins can only supply a maximum of 12mA of current. The LED I used has a forward voltage of 1.8V (Vf in the datasheet) and a forward current (If) of 25mA. Applying some Ohms we can calculate the ideal resistor value as follows: Voltage drop (assuming 3.3V from the NodeMCU) = 3.3V - 1.8V = 1.5V Resistor value = 1.5V / 25mA = 60Ω The closest resistor value I had was 100Ω. But I found the LED to be a bit too bright so I ended up using a 220Ω resistor instead. This is well within the NodeMCU's maximum current-draw of 12mA. If you're interested in learning more about how to calculate resistor values you can read about it [here](https://www.evilmadscientist.com/2012/resistors-for-leds/). The R2 resistor is used to pull the PIR-sensor's output pin high. This is because the PIR-sensor I used has an open-collector output. I won't go into detail on how open-collector outputs work but if you want to learn more you can read about it [here](https://en.wikipedia.org/wiki/Open_collector). If you're using a PIR-sensor with a push-pull output you can remove the R2 resistor. The R3 resistor is used to pull the button's input pin low. When the button is pressed the input circuit is closed and the input pin is pulled high. Both the R2 and R3 resistors should be between 1kΩ and 10kΩ. I used 10kΩ resistors for both. #### Power draw Estimating the power draw of the circuit is a bit tricky. We would have to look at the power draw for each individual component and add them all together. But since the LCD-display and LED are only on for certain periods of time we can't just add them together. We would have to calculate the average power draw for each component and then add them together or measure the power draw of the entire circuit using a multimeter or usage monitor (like Kill A Watt) over a longer period of time. I'll update this section if I get around to it. ### Platform I chose to self host everything on a dedicated Ubuntu VM running on my home Proxmox VE (an open-source hypervisor) server. But everything could easily be hosted on a Raspberry Pi or any other Linux machine. Every application in the stack is running in a separate [Docker](https://www.docker.com/) container. The containers are managed using [Docker Compose](https://docs.docker.com/compose/). This makes admin and maintenance a lot easier since I can tear down and rebuild the entire stack with a single command. The server stack consists of the following: * [Eclipse Mosquitto](https://mosquitto.org/) - (MQTT-broker) * [Node-RED](https://nodered.org/) - (Flow-based programming) * [Telegraf](https://www.influxdata.com/time-series-platform/telegraf/) - (Data collector) * [InfluxDB](https://www.influxdata.com/) - (Time-series database) * [Grafana](https://grafana.com/) - (Dashboard) The last three form what's commonly referred to as the TIG-stack (Telegraf, InfluxDB, Grafana). #### Eclipse Mosquitto Mosquitto is an open-source MQTT-broker. It's used to route MQTT messages between the Box Unit and the other applications in the stack. Mosquitto is a very easy to set up and use and it's very well documented. It's also very lightweight and doesn't require a lot of resources. This makes it a great choice for a small project like this. #### Node-RED Node-RED is a flow-based programming tool. It allows for easy integration between different applications and services. It's a very popular automation-tool for self hosted projects and IoT. I use it to integrate with Telegram to send notifications to my phone when the box needs to be cleaned. #### Telegraf Telegraf is a server-based agent used to collect and send data to a database. I use it to collect data from the MQTT-broker and store it in a database. It can also be used to collect system metrics from the host machine among other things. It's a very powerful tool with a lot of plugins. #### InfluxDB InfluxDB is a time-series database. I use it to store the data collected by Telegraf. Since the data is tracked over time it makes sense to use a time-series database. InfluxDB is one of the most popular time-series databases and plays nicely with other tools in the TIG-stack. #### Grafana Grafana is a data visualization tool. It can be used to create dashboards and graphs from the data stored in the database. I use it to visualize the data from the box unit. ### Setting up the server Setting up the server is quite easy if you have some experience with Docker and Linux. If you don't have any experience with Docker or Linux it might be a bit tricky. I will not go into detail on how to set up each application but I will give a brief overview of the steps needed to set up the server. If you're interested in learning more about how to set up each application you can find more information in the GitHub repository. The process of setting up the server can be split up into the following steps: 1. Install Docker and Docker Compose 2. Clone the GitHub-repository 3. Edit the configuration files 4. Run `docker-compose up -d` to start the containers 5. Configure the applications using the web interfaces ### The code The code is split up into two files and a library directory. The main file is called ```main.py``` and contains the main loop. The other file is called ```boot.py``` and contains code that runs when the NodeMCU boots up. The library directory contains the MicroPython libraries for the I2C LCD backpack and configuration file. The libraries are not written by me and can be found [here](https://github.com/dhylands/python_lcd/). I won't go into detail on how every part of the code works since it's quite long and complicated. But I will give a brief overview of the different parts of the code and a few examples of interesting problems I encountered. On boot the board connects to the WiFi and performs a connectivity test. If the test is successful it will synchronise the time with an NTP-server, this is important since the events are compared with data in the database. When the time has been synchronised it will start the main function. The main loop runs every `x` seconds and performs the following steps: 1. Read the temperature and humidity from the DHT11 2. Handle any IRQs from the PIR-sensor or button 3. Send the data to the server over MQTT 4. Update the LCD-display 5. Sleep for `x` seconds The period of sleep is configurable and can be changed in the configuration file. The default value is 30 seconds, but could be set to something longer if operating on battery power. #### IRQs IRQs are interrupts that are triggered when a certain event occurs. In this case the events are either the PIR-sensor detecting motion or the button being pressed. When an IRQ is triggered the corresponding function is called. The function then sets a flag which is checked in the main loop. This allows for "asynchronous" handling of events. The main loop doesn't have to wait for the event to occur, it just checks if the flag has been set. #### Handling global state To avoid defining global variables I used a class to store the global state. This allows me to access the global state from anywhere in the code without having to define global variables just by passing the class as an argument to the function. Since classes are passed by reference any changes made to the class will be reflected in the global state. This was enormously helpful when handling IRQs. I could just pass the state class to the IRQ handler and set the flag in the class. The main loop would then check the flag and perform the necessary actions. Example of how the state class is used: ```python pir.irq(trigger=Pin.IRQ_FALLING, handler=lambda _: pir_handler(state)) ``` Notice how the handler funciton is actually a lambda function. This is because the handler function of the IRQ takes the pin as an argument. Since we don't need the pin we can just ignore it by using a lambda function and passing the state class as an argument instead. The `pir_handler` function could then set the flag in the state class like this: ```python def pir_handler(state: SensorState): state.motion_detected = True ``` #### Long and short press detection To detect both long and short presses I used a timer. When the button a function is called which starts a timer. This function can determine whether the button was pressed for a short or long time by checking how much time has passed since the timer was started. Depending on whether the button was pressed for a short or long time a different callback function is called. This also handles debouncing of the button, preventing multiple events from being triggered by a single button press. This is by far the most complicated part of the code, but the end result is quite elegant. If you're interested in learning more about how it works you can check out the code in the GitHub repository. Here's an example of how the timer is used: ```python timer.init( period=100, mode=Timer.ONE_SHOT, callback=lambda x: button_handler( timer=x, state=state, button=button, long_press_time=2000, short_press_callback=short_press_handler, long_press_callback=long_press_handler, ), ) ``` #### Getting the last event from the database When the box is powered off it loses track of time. This means that when it's powered on again it doesn't know when the last event occurred. This is a problem since the box needs to know when the last event occurred in order to determine whether it needs to be cleaned or not. To solve this problem I added a function that gets the last event from the database. This involves running a manual HTTP query against the database. Since the response from the database is in CSV format I had to parse the response and convert it to a timestamp. This was a bit tricky but I managed to get it working. The function returns a timestamp which is then used to determine whether the box needs to be cleaned or not. ### Transmitting the data / connectivity The data is transmitted to the server over MQTT at regular intervals defined in the configuration file. The data is sent as a JSON object containing the temperature, humidity, motion and button state. All the data is sent in a single message. This is done to reduce the number of messages sent to the server. The messsage looks something like this: ```json { "humidity": 36, "reset_pressed": 0, "motion_detected": 0, "temperature": 26 } ``` Using WiFi was an obvious choice since the NodeMCU has a built-in WiFi module and the device is always indoors, in range of my WiFi network. I chose to use MQTT since it's a very lightweight protocol and is well suited for IoT. If running on battery power it's possible I'd change strategy but since the box is powered by a USB-cable I don't have to worry too much energy consumption. ### Presenting the data InfluxDB has a built-in retention policy which allows you to specify how long data should be preserved in the database. I've set the retention policy to 1 year. This means that data older than 1 year will be deleted from the database. This is more than enough for my use case. All the data is presented in a single dashboard. It plots the temperature and humidity over time and shows the current state of the box (last cleaned and last used). ![](https://i.imgur.com/sjT6cUr.png) I've also created a flow in Node-RED which allows me to get the current status of the litterbox via Telegram. ![](https://i.imgur.com/FsFBfDF.png) ![](https://i.imgur.com/eJ7rwbU.png) Additional monitoring could easily be added now that everything is in place, such as alerts when the box needs to be cleaned or when the box hasn't been used for a long time. ### Finalizing the design #### Results The system works as intended. The box unit gathers data about the litter box and sends it to the server. The server stores the data in a database and visualizes it using a dashboard. Here are some pictures of the box unit ![](https://i.imgur.com/hfOKo66.jpg) Mounted on the litter box ![](https://i.imgur.com/s45rup1.jpg) #### Video {%youtube -1Permz3gXA%} #### Circuit I'm in the proccess of soldering the final circuit together on a perfboard. In the meantime I'm using a breadboard. I have some pictures of an earlier version of the circuit soldered together on a perfboard, the final version will look similar to this: ![](https://i.imgur.com/rDJXYCe.jpg) #### Reflections The project was a success. I managed to create a system that gathers and stores data about a cat litter box and reminds me to clean the box when needed. I also managed to create a dashboard to visualize the data. I'm very happy with the result and I'm looking forward to using the system. Some of the more advanced insights I wanted to get from the data will have to wait until I've gathered more data. But I'm confident that I'll be able to get some interesting insights from the data in the future. I managed to fry my microcontroller the second to last week of the project. I'm not sure what happened but I think I might have shorted something when I was soldering the circuit together. I had to order a new one which took a few days to arrive. This meant that I had to rush the last week of the project. I didn't have time to implement all the features I wanted to and only got to building the dashboard/visualizations at the very end. But I managed to get everything working in time for the deadline by working a few late nights. #### Future work If I had more time I would have designed and printed a sensor unit as well, some sort of enclosure which bundles the DHT11 and PIR-sensor together and provides an easy way to mount the sensors to the litter box. The PIR-sensor is currently attached by only electrical tape, which is not ideal. I also have a load cell and HX711 amplifier which I wanted to use to measure the weight of the litter box. I would have used this data to determine how much litter is left in the box. This would allow me to predict when I need to refill the box. Unfortunately I didn't have time to implement this feature. I might add it in the future. UPDATE: I modeled a sensor unit and changed to a DHT22 https://i.imgur.com/ezSjrKY.mp4

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully