Image for post
Image for post

Juggling Sigfox Downlink And Arduino Sensors With cocoOS

In the previous article “” we looked at the for task scheduling and how we may read data from multiple sensors through concurrent Sensor Tasks. Today we’ll complete the Arduino application by adding the concurrent Network Task and UART Task for sending the sensor data to the Sigfox low power wide area network.

We’ll also be looking into Sigfox Downlink, which allows messages to be pushed from the cloud to the device. Sigfox may take up to 1 minute to return the downlink message, so the cocoOS task scheduler is perfect for running the Sensor Tasks concurrently while waiting for the Network and UART Tasks.

Image for post
Image for post
Our proposed Arduino IoT Device and the concurrent tasks

Here’s the hardware that we’ll be using for our IoT device…

  1. Arduino Uno
  2. Temperature Sensor ()
  3. Humidity Sensor (same BME280 I2C as above)
  4. Altitude Sensor (same BME280 I2C as above)
  5. Wisol Sigfox Transceiver for sending the sensor data to the Sigfox network and receiving Sigfox downlink data ( , , or ). Supports UART (or Serial I/O) interface at 9600 bps. Connected to the Arduino Uno on pins D4 (transmit) and D5 (receive).

The last 4 items may be replaced by the .

Image for post
Image for post
Tasks, Messages and Events in the send_altitude_cocoos pattern

The send_altitude_cocoos code referenced in this article is a Reusable Pattern that may be copied and pasted into our own Arduino projects. We built this pattern optimised for memory usage and concurrency on the Arduino Uno, reading from multiple sensors and sending data to Sigfox through concurrent tasks. The five send_altitude_cocoos tasks are…

  • : Reads the BME280 I2C Temperature Sensor repeatedly. Sends the Sensor Data message to the Network Task.
  • : Reads the BME280 I2C Humidity Sensor repeatedly. Sends the Sensor Data message to the Network Task.
  • : Reads the BME280 I2C Altitude Sensor repeatedly (not shown in the diagram). Sends the Sensor Data message to the Network Task.
  • : Receives sensor data from the above Sensor Tasks, aggregates the sensor data, throttles the sending of the aggregated sensor message, and sends the Wisol AT Command String to the UART Task. The code here is specific to the Wisol Sigfox module.
  • : Receives a Wisol AT Command String from the Network Task and sends it to the UART port (or serial I/O port). The response from the command is returned to the Network Task. The Success and Failure Events are triggered upon sending success/failure so that the Network Task can be suspended until the sending is completed.

In this article we’ll explain how send_altitude_cocoos aggregates data from multiple sensors and sends the aggregated data to Sigfox with throttling. We’ll also go into detail how to set up a Downlink Server to send Sigfox downlink messages. send_altitude_cocoos has been optimised for the constrained memory of the Arduino Uno, we’ll also cover that. All the code may be found here…

Sending Sensor Data (but not too often)

With Sigfox we can pack up to 12 bytes of sensor data into a single message. It’s not a lot of space, but with some creative packing, it’s actually sufficient for many use cases. Today we’ll use a simple format that’s easy to read and troubleshoot, but not optimised…

00010347050923840001 = Running message sequence number: 0, 1, 2, ...
0347 = Temperature scaled by 10: 34.7 deg C
0509 = Relative humidity scaled by 10: 50.9 %
2384 = Altitude scaled by 10:
238.4 metres above sea level

Each sensor value occupies 4 hexadecimal digits (again, it’s not optimised). Adding up to 16 hex digits, or total 8 bytes (with room for 4 more bytes).

Here’s a simple trick to send decimal or floating point numbers: We scale the values by 10 times to preserve one decimal place. So instead of sending 34 for the temperature, we send 347 to the cloud and later downscale by 10 to get the actual value, 34.7. One decimal place is sufficient for most use cases.

How often shall we send the sensor data? Sigfox and other similar LPWANs operate on unlicensed, shared, free-to-use radio frequencies. To ensure that everyone can use the airwaves fairly, we limit our transmission to 140 messages a day, roughly one message every 10 minutes.

Which means that we need to be smart about the way we aggregate our sensor data. We can’t possibly send all the data sampled from our sensors, but we could aggregate them in a meaningful way for transmission.


Aggregating Sensor Data

In this demo we have configured our sensors to be polled every 5 seconds . The aggregate_sensor_data() function in lets you implement your own logic for aggregating the sensor data. You could…

  1. Send only the latest value from each sensor. Throw away all the past sensor data. Which is done in the code above.
  2. Or send only when the sensor data has exceeded a threshold e.g. temperature > 35.0. The downlink feature that we’ll discuss below will be very useful for updating the threshold on the fly.
  3. Or send the maximum / minimum / average value of each sensor, computed over the past 10 minutes. The sensorData array above has sufficient space to let you to store and compute these simple aggregated values.
if ((context->lastSend + SEND_INTERVAL) > now) 
{ return false; } // Not ready to send.

Implemented in aggregate_sensor_data() is a very simple throttle feature that prevents messages from being sent too often. SEND_INTERVAL (defined in ) is the number of milliseconds that we should wait before sending a new message.

Now let’s understand how we may use the Wisol Sigfox module to send messages to the Sigfox network…

Controlling the Wisol Sigfox Transceiver (like it’s 1981)

Image for post
Image for post
Wisol Sigfox Transceiver Module

The Wisol transceiver is connected to the Arduino via the Serial I/O port (also known as the UART port). It accepts AT Commands (originally invented by Hayes in 1981!) like these:


After each AT command the transceiver returns a response like OK. Above is the list of AT commands that we normally send at startup to disable the Sigfox emulation mode (ATS410=0), fetch the Sigfox device ID (AT$I=10) and fetch the Sigfox PAC code (AT$I=11), which is used for activating the Wisol module on first use.


How do we wrap these AT commands into a pattern that may be reused for other transceivers? Check the code above — we bundle the three commands into a Network Step, like the Begin Step above.

addCmd(..., { F(CMD_GET_ID), 1, getID, NULL, NULL } ...

Each call to addCmd() adds a command to the definition of the Begin Step, with the following parameters:

  1. CMD_GET_ID is the AT command, i.e. ATS410=0. The command is wrapped inside F(...) so that it’s stored in Flash Memory instead of Dynamic Memory. (Refer to the section )
  2. 1 is the number of end-of-line markers (i.e. the carriage return character \r) that we expect to receive for the response of this command. The UART Task counts the number of \r characters received to decide when to return the response.
  3. getID() is the Response Processing Function — the function that’s called to parse the response from the AT command e.g.002C2EA1. For this command the getID() function extracts the Sigfox Device ID from the response and saves it into the Network Task context for use later.

The Send Step is called by the Network Task to send messages to the network. This Step calls addCmd() with two additional parameters…

addCmd(..., { F(CMD_SEND_MESSAGE), 2, getDownlink, 
} ...
  • payload contains the 12-byte message to be sent to the Sigfox cloud, represented as 24 hexadecimal digits, e.g. "000103470509238401234567"
  • CMD_SEND_MESSAGE_RESPONSE is the suffix that will be appended to the AT command, i.e. ",1". This parameter is used only when requesting for Sigfox downlink.

The complete AT command that will be sent looks like this… (CMD_SEND_MESSAGE is defined as "AT$SF=")


That’s an actual Wisol AT command to send the 12-byte message 000103470509238401234567 (with downlink requested) to the Sigfox. You can see the AT commands in action at the “”.

The Network Task was designed to support different UART command sets if we wish to apply the send_altitude_cocoos pattern to a different network transceiver. But if we were to use the instead, we would have to send SPI commands to an SPI Task (which we’ll have to create) to control the InnoComm module.

Now let’s look at the Network Task code to understand how we read and send the sensor data concurrently…


Network Task for sending and receiving network messages

network_task() shown above (from ) loops forever waiting for sensor data and transmits them. Each Sensor Task (temperature, humidity, altitude) sends a message to the Network Task containing a realtime sensor value like this (defined in ):

name:     "tmp"    (Name of the sensor, i.e. temperature)
data[0]: 34.7 (Array of sensor data floating-point values)
count: 1 (Number of values)

The sensor data message triggers the following in Network Task…

  1. network_task() passes the sensor data message to aggregate_sensor_data(), which we have discovered earlier.
  2. aggregate_sensor_data()aggregates the past and present sensor data into a message. Based on the throttle logic, it decides whether to send the aggregated message now.
  3. If it returns true, network_task() takes the aggregated message returned in cmdList (a list of Wisol AT Commands) and sends each AT command, one at a time (the inner loop).
  4. For each AT command, the function convertCmdToUART() converts the AT command like…
    F(CMD_SEND_MESSAGE), 2, getDownlink,
  5. …into a plain string for sending over UART like this…
  6. network_task() calls msg_post() to send the UART string to the UART Task for transmission.
  7. Process the UART response. Repeat until each AT command has been transmitted.

In the previous article we have created Sensor Tasks using the task_open() ... task_close() functions from cocoOS. Also we have created a Semaphore to lock the I2C Bus by calling sem_wait() ... sem_signal(). You’ll find these in network_task() too.

Previously we used msg_post() for sending messages between tasks. Here we are using a new way for the Network and UART Tasks to communicate: via cocoOS Events.

Events are useful when you need to wait for another task to complete processing before you continue processing something else. In the above code, the Network Task sends a UART Command to the UART Task and waits for the UART Command Response to be received.

event_wait_multiple(0, successEvent, failureEvent);

This line pauses the execution of the Network Task and waits for the UART Task to trigger a Success Event or Failure Event when the UART Command has been completed. To trigger an event, the UART Task calls the cocoOS function event_signal()


This resumes the execution of the Network Task so that it may proceed to the next AT command.

//  From network_task()...
// Use a semaphore to limit sending to only 1 message
// at a time, because our buffers are shared.
// Wait until no other message is being sent.
// Then lock the semaphore.
// Send each Wisol AT command in the list.
for (;;) {
// Send the UART command thru the UART Task.
msg_post(uartTaskID, uartMsg);
} // Loop to the next Wisol AT command.
// Release the semaphore and allow another payload to be sent.

Why use a Semaphore?

A Semaphore is used to prevent concurrent access to shared resources. Like in the Sensor Tasks, we can’t allow concurrent access to the I2C Bus, so we used a Semaphore to limit the access.

Q: Why do we use a Semaphore sendSemaphore in network_task()?

A: Because the Arduino Uno has severe memory limitations (only 2KB of RAM). We can only afford to keep ONE memory buffer for sending and receiving messages. So the Semaphore prevents the Network Task from reusing the shared memory buffer before it’s done.

UART Task for sending and receiving serial port data (like it’s 1960)

The UART Task is triggered by the Network Task to send an entire AT Command String over the UART port to the Wisol module at 9,600 bits per second, one character at a time. And then to receive the response one character at a time. (FYI: RS-232 serial comms has been in use since 1960!)

At 9,600 bits per second we expect to send/receive a character roughly every 0.8 milliseconds. Which gives us some time to multitask and read sensor data. The Arduino Uno connects to the Wisol module via a SoftwareSerial driver on pins D4 (transmit) and D5 (receive), so it’s safer to send out the characters at a slower interval than 0.8 milliseconds in case the buffer overflows.


In the uart_task() function you can see a few spots in which we call task_wait(). This signals to the cocoOS task scheduler that it’s OK to switch to a Sensor Task while sending or receiving UART data.

How does the UART Task know when to stop receiving data?

  1. When the timeout has been reached. Each UART request from the Network Task includes a timeout value, like 20 seconds (UPLINK_TIMEOUT). Downlink requests will have a higher timeout value, like 60 seconds (DOWNLINK_TIMEOUT). Timeout values are defined in
  2. When we have received a specified number of end-of-line \r characters. Each UART request also includes the expected number of \r characters that will be received
Sigfox Uplink and Downlink Sample Log from

cocoOS multitasking works very well for reading and sending sensor data concurrently — check the “”.

1. You can see that all three sensors (temperature, humidity and altitude) are polled repeatedly at the top of the log.

2. After waiting for a while (to throttle the message sending), the Arduino sends this command to transmit the sensor data message (and request Sigfox Downlink)…

>> AT$SF=0003033705232434,1

3. The downlink message will take 1 minute to be returned. Meanwhile the Arduino continues to poll all three sensors repeatedly. So it continues to aggregate sensor data while waiting for the downlink message.

4. Finally the downlink message is received…

<< OK … FE DC BA 98 76 54 32 10

We’ll now go deeper into the Sigfox Downlink protocol.

Sending A Downlink Request To Sigfox

With Sigfox Downlink we can actually push a message from our server down to our Sigfox device — just like Push Notification. Our device needs to initiate the Downlink Request or Sigfox won’t send the downlink message.

The Downlink Request looks very similar to the AT command for transmitting Sigfox message. For example…


This is the command you normally see when the Arduino sends the message 000103470509238401234567 to Sigfox. For downlink there’s an extra parameter…


See the ,1 at the end? That tells Sigfox that we expect a downlink response. Sigfox will then call our Downlink Server (via the Sigfox callback) to get the response for the downlink. Then the downlink response will appear in about 1 minute

RX=FE DC BA 98 73 54 32 10

Downlink messages can have up to 8 bytes (instead of 12 bytes for normal messages). So in the above example, the downlink message is the 8 bytes FEDCBA9873543210.

A downlink request is more difficult to execute because unlike normal Sigfox messages we can’t just broadcast a downlink message — Sigfox needs to locate the basestation nearest to our device and use that basestation to send the downlink message, hence the 1 minute delay.

You’ll see this error when downlink fails due to poor network coverage or other issues:ERR_SFX_ERR_SEND_FRAME_WAIT_TIMEOUT. You can see this in the “”.


To insert our Arduino code for processing the downlink message, check out the process_downlink_msg() function in .

Our Downlink Server

The Downlink Server protocol is HTTP, just like the usual Sigfox callback. Except that we must return a HTTP JSON response that contains the downlink message. We could implement the Downlink Server on the Amazon Cloud using like this…

The above code is in Node.js. When a device sends a message, Sigfox delivers a JSON request like…

{ "device": "2C2EA1", "data": "000103470509238401234567", ... }

The AWS Lambda code above will extract the device ID 2C2EA1 and insert into the response message…

{ "2C2EA1" : { "downlinkData" : "fedcba9876543210" } }

The downlink data is hardcoded here — fedcba9876543210. With AWS Lambda and Node.js code we could connect to a database, connect to another server via REST, … to compose the downlink response. As long as we can fit within 8 bytes.

Configuring the Sigfox Downlink Callback

We need to add a Sigfox callback for the downlink. Log in to the Sigfox Backend: . Click Device Type, select your device type, click Callbacks.

Image for post
Image for post
Sigfox Callback for downlink

Click New to create a Sigfox callback. Select Custom Callback. Use these settings to configure the callback…

Image for post
Image for post
Sigfox Callback for downlink

For testing, you may use my Downlink Server URL as shown above:

This is hosted on my personal Amazon Cloud account and it has been hardcoded to return the downlink message fedcba9876543210 for any device ID. I hope this helps to make your Sigfox Downlink development and testing easier.

Image for post
Image for post
Click the circle in the “Downlink” column

Make sure you click the circle in the Downlink column. It should be a filled circle, not an empty circle.

Image for post
Image for post
Sigfox Downlink Messages

At the Sigfox Backend Messages Log you can see that the downlink message looks different from normal messages — there are now two arrows in the Callbacks column. If you mouse over the second arrow you’ll see the Downlink Status. Good for troubleshooting problems with our Downlink Server.

Depending on the Sigfox subscription plan, Sigfox guarantees only 4 downlink messages per day. We can send more downlink requests, but Sigfox is not obligated to deliver. But from my experience, virtually all downlink messages are delivered if the network coverage is good. So just go ahead and try it out!

If you don’t require multitasking for the Sigfox Downlink and you’re happy to have the Arduino hold up all execution for 1 minute while waiting for the downlink, you may take a look at this sample Arduino sketch for Sigfox Downlink. It’s single-threaded and it’s a lot simpler than send_altitude_cocoos.

Image for post
Image for post
Pending Response in the send_altitude_cocoos pattern

Why don’t we wait for all responses? Why use Events vs Messages?

When the Network Task sends a message to Sigfox, the response may take up to 1 minute to be returned. This could cause the Network Task to be suspended for 1 minute, since we use a Semaphore to allow only one message to be sent at a time. This means that the Network Task will be frozen for 1 minute and it can’t even aggregate any sensor data, since that function is called by the Network Task.

So we created a special flow for handling the sending of messages only—we don’t wait for the UART Task to complete the transmission of messages to Sigfox. (We always wait for the UART Task to complete for other AT commands e.g. Get Sigfox ID.) When the message transmission is completed, the UART Task sends a Pending Response message (using the Sensor Data format) to notify the Network Task.

Recall that for most AT commands, the UART Task signals the Success and Failure Events to the Network Task. Now we have a special case that sends a Pending Response message to the Network Task. When do you use cocoOS Events vs Messages?


It depends on our coding style: Synchronous (Events) or Asynchronous (Messages). Look at the above snippet from network_task() — the flow looks logical: send a command, wait for command to complete, move to next command. The event_wait_multiple() function by cocoOS will suspend the Network Task until the UART Task triggers the Success Event or Failure Event.

The code is easier to understand when we use Events, but it prevents the task from doing anything else when waiting for the Event. This is OK for short commands that complete within a few seconds, but the downlink request could take up to a minute to complete.

That’s why we use a Message instead of an Event while waiting for slow commands. The code then becomes more complex because we always need a task loop (like network_task()) to process Messages.

Compiling The Arduino Code

The code in this article may be downloaded from the send_altitude_cocoos repository above. It assumes that you have a BME280 module connected to the Arduino Uno via the I2C Bus, but you may comment out the BME280 code and use the simulated values instead (search for Simulated sensor in the files *_sensor.cpp) It also assumes that you have a Wisol Sigfox module connected on pins D4 (transmit) and D5 (receive).

The code above compiles under the Arduino IDE, , and with the extension installed. You’ll need to install the following libraries:

Normal Arduino sketches have a setup() and a loop() function. Ours is different — it starts at the main() function defined in . Because our sketch defines the main() function, the setup() and loop() functions will be ignored.

Edit the file platform.hand uncomment the following line so that you’re using the right features for this article…

#define CONFIG_ARTICLE2  //  Uncomment to support Article #2

Check that the other #define CONFIG_ARTICLE... lines are commented out.

Arduino IDE

Image for post
Image for post
Arduino IDE

To open send_altitude_cocoos in Arduino IDE:

  1. Browse to
  2. Click Clone Or Download then Download Zip
  3. Unzip the downloaded file
  4. Look for the send_altitude_cocoos_master folder. If you see another send_altitude_cocoos_master folder inside that folder, use the inner folder.
  5. Rename the folder to send_altitude_cocoos
  6. Open the folder. Double-click the Arduino sketch send_altitude_cocoos.ino

Arduino Web Editor

Image for post
Image for post
Arduino Web Editor showing the source tree, source window and compiler log

The Arduino Web Editor is a browser-based development tool that stores and compiles your code in the cloud. It talks to your Arduino device using a driver that’s installed on your PC.

To load the send_altitude_cocoos code directly into Arduino Web Editor, just click the link below:

If you haven’t tried Arduino Web Editor, click the link and test it out! I highly recommend it for beginners. My IoT students seem to have fewer problems with the Arduino Web Editor than with the Arduino IDE (e.g. unable to detect Arduino on USB, incorrect file extensions).

The link above contains a snapshot of the send_altitude_cocoos code, not linked to GitHub. If you wish to use the latest code from GitHub:

  1. Browse to
  2. Click Clone Or Download then Download Zip
  3. Unzip the downloaded file
  4. Look for the send_altitude_cocoos_master folder. If you see another send_altitude_cocoos_master folder inside that folder, use the inner folder.
  5. Rename the folder to send_altitude_cocoos
  6. Zip the renamed folder into
  7. In Arduino Web Editor, click Sketchbook in the navigation bar and upload

Visual Studio Code with PlatformIO

Image for post
Image for post
Visual Studio Code + PlatformIO showing the source tree, multiple source windows and Serial Monitor

Visual Studio Code with PlatformIO is a client-based IDE like the Arduino IDE. But the auto-completion and code navigation features make complex Arduino programming so much simpler.

I recommend Visual Studio Code with PlatformIO (the free version) if you’re doing serious Arduino programming with many modules. I used it to develop the code for send_altitude_cocoos.

Image for post
Image for post

PlatformIO requires the send_altitude_cocoos code to be located in the src subfolder. Follow the instructions below to create the src subfolder and the symbolic links to the source files (see “Create Source File Links for PlatformIO”):

The cocoOS library folder should be copied into the libsubfolder.

Within the cocoOS folder, create an src subfolder. All cocoOS source files (*.h, *.c) should be moved into the srcsubfolder.

Follow the README instructions above to link the file os_defines.h with the custom cocoOS settings for our project.

Arduino Memory Size Helper for Memory Optimisation

The Arduino Uno has severely limited Dynamic Memory (only 2 KB of RAM) so we had to do lots of memory optimisation to get the send_altitude_cocoos code to run correctly. (What’s Dynamic Memory? See the section )

Here’s what we did…

  1. We customised the cocoOS settings to reduce the memory footprint because we did’t need to support so many tasks:
  2. Memory buffers for storing sensor data and message data are defined as static arrays. We don’t use the heap and stack to create new buffers. So when our Arduino application runs, the memory usage stays fairly constant and the application won’t crash due to lack of heap and stack space.
  3. We maintain only ONE shared memory buffer for composing network messages. To avoid clashes, we use a Semaphore to allow only one task at a time to access the shared memory buffer.

How did we identify the large objects in memory? We created a Google Sheet — Arduino Memory Size Helper — to identify and optimise large objects…

  1. In Arduino IDE, set this preference: Enable Show Verbose Output During Compilation
  2. In Arduino IDE, build your application
Image for post
Image for post

3. In the build log window, copy the text from Linking everything together... to the end of the log.

4. Click on the Google Sheet below. Click File → Make A Copy.

5. Paste the build log into the space marked Paste your Arduino IDE Build Log

Image for post
Image for post

6. The spreadsheet locates the ELF file path and generates two gobjdump commands. Run the two gobjdump commands shown in your spreadsheet. They will generate two files, bss.csv and data.csv.

Image for post
Image for post

gobjdump for Mac OS X:

gobjdump for Ubuntu on Windows 10:
sudo apt install binutils ; alias gobjdump=objdump

Image for post
Image for post

7. Click File → Import → Upload. Select bss.csv. Use these import settings.

8. Import data.csv the same way.

9. Go to the imported bss.csv sheet. Delete the top few rows until (and including) SYMBOL TABLE

10. Select the entire sheet. Change the cell format to Plain Text

Image for post
Image for post

11. Click Data → Split Text To Columns. Leave the Separator as Detect Automatically.

Image for post
Image for post

12. Delete Columns B, C, D

13. Insert Column C. Type this formula into cell C1 to covert the hexadecimal number in Column B to decimal. Copy and paste the formula to the entire Column C.


14. Click Data → Sort Sheet By Column C, Z>A

Image for post
Image for post

Repeat for the data.csv sheet. The result looks like this:

Image for post
Image for post

This sheet lists all the objects stored into the BSS (or Data) sections of Dynamic Memory. Column C is the size of each object (in bytes) and Column D is the name of the object.

The largest objects are shown at the top of the sheet. These are the objects that we should fix to reduce memory usage. By searching for the names of these objects (e.g. uartMsgPool) in the send_altitude_cocoos source files, we’ll be able to discover how these objects are used and how we can reduce their sizes.

Image for post
Image for post
STM32 Blue Pill microcontroller

What’s Next?

This might be the first time that anybody has created a Best Practice or Pattern for an Arduino IoT application that reads and sends sensor data optimally, with proper multitasking and memory allocation. It’s amazing what we could accomplish on the Arduino Uno with Peter Eckstrand’s highly-efficient cocoOS library. I hope IoT makers and learners will find the send_altitude_cocoos code useful.

With help from Peter, I have started porting the send_altitude_cocoos code to the , a very popular and very affordable microcontroller that’s cheaper than the Arduino Uno and yet more powerful. I’m extremely grateful that Peter has designed cocoOS to be portable across multiple platforms, including Arduino and STM32. Since cocoOS insulates the application from the multitasking details, it’s really helpful for migrating Arduino applications to STM32 Blue Pill.

Check out the details here…

A Quick Poll / 快速民意调查 / 快速民意調查

Would you be interested in a Simplified Chinese 简体 / Traditional Chinese 繁体 translation of this Sigfox/Arduino article?

Or my ?

Or other languages?

Please post a comment here thanks!


Coinmonks is a non-profit Crypto educational publication.

Sign up for Crypto News

By Coinmonks

A newsletter that brings you week's best crypto and blockchain stories and trending news directly in your inbox, by Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Techie and Educator in IoT 物聯網教師

Coinmonks is a non-profit Crypto educational publication. Follow us on Twitter @coinmonks Our other project —

Lup Yuen Lee 李立源

Written by

Techie and Educator in IoT 物聯網教師



Coinmonks is a non-profit Crypto educational publication. Follow us on Twitter @coinmonks Our other project —

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store