< RETRO BLOG >

Reverse Engineering of the SkyRC MC3000 Battery Charger USB Protocol

by jpk on 2019-02-18 15:49:54 edited on 2019-02-18 16:01:20

Reverse Engineering the MC3000 Charger Tool

Software Requirements

Software Purpose
dotPeek Decompiler for .NET programs

The implementation of the protocol was then written in Python.

Let's hear what the curator has to say:

Tell me about Python.

"Wow. Much Snake. Easy programming!"
                              - Doge

Tell me about dotPeek.

"Easy decompilation. Readable syntax. Very easy."
                              - Doge

Preparation of your Environment

Analysing MC3000_Monitor

Decompiling

Start dotPeek and use Drag'n'Drop to load the Application. Or uSe CtRL+O anD LoCATe tHe fILe uSiNg ThE bOrwsEr. I am not your mother! The file will show up in the Assembly Explorer. You can expand and collapse nodes within the tree. Familiarize yourself with the program structure. Try to find the entry point or other "important" functions, modules or resources.

/upload/get/5c691e64_0003.png

If you right click the MC3000_Monitor node you can export the project with individual source files. This project is stored as *.cs source files.

You should now have either the project loaded into dotPeek or - in addition - saved it as project and/or loaded it into Visual Studio (VS) (Not covered here). I cannot afford VS. Still saving money to upgrade my IDA Pro license.

Intermission:
This is the curator speaking, and I command you to stop whining, 'JPK'.

As you can see, a lot of professionalism.

Exploring the code

For me the easiest way to begin, is to find parts of code where user interaction is required. Run the program and look at the user interface. In this particular case we have four buttons next to a label each.

Lets explore the code in dotPeek and see if we can find some code that might look familiar.

/upload/get/5c691e4e_0002.png

Pressing one of the buttons opens another window where you can configure the charger slot. By further reading through the different functions you might come across the function InitializeComponents():void. Each window element gets setup and function callbacks/events are registered.

You eventually find something like this (see the picture below).

/upload/get/5c6927ae_0006.png

Let's put on our smart looking spec ticals and read line 3468 and 3470. Line 3468 is the creation of the button text, which should look familiar. If not, search for hints on this page. Line 3470 binds a function to the button press. With a Ctrl+Left click we jump to the function definition in dotPeek.

The function private void button1_Click_1(object sender, EventArgs e) is pretty simple to read. When the button is clicked, get the button name (e.g. "button1" [1]) and check if there is either the number 1, 2, 3 or 4 in the name.

/upload/get/5c691e8d_0007.png

Can you see the problem here? There is no error handling if button number is smaller than one or greater four. As an array is indexed, the program will probably crash. At this point we don't care. We want to make our own library, to make it better or different. After the name checking to know which slot is addressed, it calls a function public void Set_Battery_Type_Caps(ChargerData[] data, int index). The functions sets the parameters of each battery slot and saves the values to an array.

This function sums up all parameters we need to know to setup a charger slot by or self. And we now know the default values. The below listing is the exception code, if anything goes wrong in the code above, but not when using an index outside bounds.

File: Battery.cs
1195 data[index].Type = 0;
1196 data[index].Caps = 2000;
1197 data[index].Mode = 0;
1198 data[index].Cur = 1000;
1199 data[index].dCur = 500;
1200 data[index].End_Volt = 4200;
1201 data[index].Cut_Volt = 3300;
1202 data[index].End_Cur = 100;
1203 data[index].End_dCur = 400;
1204 data[index].Cycle_Mode = 0;
1205 data[index].Cycle_Count = 1;
1206 data[index].Cycle_Delay = 10;
1207 data[index].Peak_Sense = 3;
1208 data[index].Trickle = 50;
1209 data[index].Hold_Volt = 4180;
1210 data[index].CutTemp = 450;
1211 data[index].CutTime = 180;

The data structure ChargerData can be looked up as well, but the above listing is a little bit easier to read.

What we haven't seen at this point were bytes transferred to or from the device.

/upload/get/5c692f6d_giphy-downsized.gif

At this point, there are multiple ways to get a good starting point on finding the functions where data is transmitted or received. One option is to look at the Assembly Explorer again for functions names of possible interest.

/upload/get/5c6931af_0014.png

These convenient looking functions. Or should I say obvious function names are obvious, are used to handle the USB device communication. Try right clicking a function to find out where it is used. I have used usbOnDataRecieved. In the below window with search results you can find a reference located in the constructor [2] of the class FormLoader.

File: FormLoader.cs
352 this.usb = new UsbHidPort();
353 this.usb.ProductId = 1;
354 this.usb.VendorId = 0;
355 this.usb.OnSpecifiedDeviceArrived += new EventHandler(this.usbOnSpecifiedDeviceArrived);
356 this.usb.OnSpecifiedDeviceRemoved += new EventHandler(this.usbOnSpecifiedDeviceRemoved);
357 this.usb.OnDeviceArrived += new EventHandler(this.usbOnDeviceArrived);
358 this.usb.OnDeviceRemoved += new EventHandler(this.usbOnDeviceRemoved);
359 this.usb.OnDataRecieved += new DataRecievedEventHandler(this.usbOnDataRecieved);
360 this.usb.OnDataSend += new EventHandler(this.usbOnDataSend);

These lines above register event handlers with an instance of UsbHidPort. An event might be connecting or disconnecting the device (line: 355-358) or transferred data (line: 359-360). There is nothing special about the connect functions, except for usbOnSpecifiedDevice... ones. There is a call to stop and stop the timer2 instance. We will look at this object in a second, but first we have a look at usbOnDataSend and usbOnDataRecieved.

File: FormLoader.cs
513  private void usbOnDataRecieved(object sender, DataRecievedEventArgs args)
514  {
515    if (this.InvokeRequired)
516    {
517      try
518      {
519        this.Invoke((Delegate) new DataRecievedEventHandler(this.usbOnDataRecieved), sender, (object) args);
520      }
521      catch (Exception ex)
522      {
523        Console.WriteLine(ex.ToString());
524      }
525    }
526    else
527    {
528      ++this.packet_counter;
529      for (int index = 0; index < 65; ++index)
530        this.inPacket[index] = args.data[index];
531      this.dataReceived = true;
532    }
533  }
580  private void usbOnDataSend(object sender, EventArgs e)
581  {
582    this.Text = "ChargeMonitor V2 Connect";
583    this.label_usb_status.Text = "USB ON";
584    this.label_usb_status.ForeColor = Color.Green;
585  }

The usbOnDataSend function is boring, and we can ignore it. There is no active sending of data to the usb device. UsbOnDataReceived on the other hand is actually doing something with an buffer of 64 bytes (line: 528-531).

When data is received an internal packet_counter is increased. Each packet has a size of 64 bytes (line: 529). The packet is copied into the inPacket array, and the dataReceived variable is set to true.

My guess is, that somewhere, something, somehow, might be, is waiting for a packet to arrive and waits until dataReceived is true. In dotPeek we can use the magic function "Find Usages" again, to find out more.

/upload/get/5c694084_giphy-downsized-1.gif

Prototyping and understanding the program

Remember the timer2 instance mentioned before? No, try to find the hint on this page

File: FormLoader.cs
1219  private void timer2_Tick(object sender, EventArgs e)
1220  {
1221    byte num1 = 0;
1222    if (!this.dataReceived)
1223      return;
1224    this.dataReceived = false;
1225    if ((int) this.inPacket[1] == 240)
1226  {
1227    if (this.bwrite_chrager_data)
1228      return;
1229    int num2 = (int) MessageBox.Show("OK!");
1230  }
1231  else if ((int) this.inPacket[1] == 95)
1232    this.Get_Charge_Data((int) this.inPacket[2]);
1233  else if ((int) this.inPacket[1] == 90)
1234  {
1235    this.Get_System_Data((int) this.inPacket[3]);
1236  }
1237  else
1238  {
1239    if ((int) this.inPacket[1] != 85)
1240      return;
1241    for (int index = 1; index < 64; ++index)
1242      num1 += this.inPacket[index];
1243    if ((int) num1 != (int) this.inPacket[64])
1244      return;

The timer2 will process incoming data send from the device to the connected computer. The code begins with comparing index 1 of the inPacket with a series of values.

By looking at the code we might be able to assume we are looking at the first bytes necessary to communicate with the device. Here are some guesses:

Value 240 (0xf0):
Confirmation sent by charger.
Value 95 (0x5f):
Get charger data by information provided in index 2, which is an number [4].
Value 90 (0x5a):
Get system data by information provided in index 3, which is a number.
Value 85 (0x55):
Do not process the packet here. Otherwise calculate the sum of all bytes in num1 and compare it to the information stored in index 64.

If these checks do not result in an premature return, the values from inPacket are copied into variables. Some variable names are recognizable and help in our guessing game to find out what this function does.

/upload/get/5c69470c_0015.png

With the looks of it we are reading battery information. As an example on how the packet is decoded, we will have a look at the following code:

this.Current[j] = (int) this.inPacket[11] * 256 + (int) this.inPacket[12];

The contents of inPacket index 11 and 12 is assigned the the variable Current at index j. Which is irrelevant at this point. But we need to understand what is happing with this multiplication and addition.

The multiplication by 256 is just a different way to express a left shift by 8. What happens, when we take the value 1 and mutliply it by 256 or do a left shift by 8? In binary representation it will become very easy to understand.

  1   =>         0b1
256   => 0b100000000

So what if we take 256 times 1?

256   => 0b100000000

And if we take the value 0b1 and move the 1 exactly 8 positions to the left, like a left shift, duh?

1 << 0 = 0b1
1 << 1 = 0b10
1 << 2 = 0b100
1 << 3 = 0b1000
1 << 4 = 0b10000
1 << 5 = 0b100000
1 << 6 = 0b1000000
1 << 7 = 0b10000000
1 << 8 = 0b100000000

This is just a step by step illustration. Computer do fast. Computer do in single step.

After the left shift, the second value is added to the variable. In other words, we are reading two bytes and concatinate it to have a word (2 bytes).

The same applies to the other functions Get_Charge_Data and Get_System_Data where the inPacket is read.

But how am I supposed to create my own library with this?

/upload/get/5c694f78_giphy-downsized-2.gif

This is the part, where you take your favorite language, or a language good for prototyping and begin coding. First challenge would be to connect to the USB device. I am using the pyusb module with Python.

To connect an USB device we want to make sure we are using the right one. To do so, the USB devices can be identified by different properties, and one of them is the vendor and product id. The source of the program might give us enough information we need, as it needs to connect to the charger as well.

The product and vendor id can be found in the function private void usbSendConnectData() and is defined as:

Vendor ID:
0
Product ID:
1

Reading Data

The people writing the firmware for the charger, did not care to give it some nice values, on the other hand 0 and 1 are nice. With these identifiers, it is possible to connect to our charger.

import usb
usb.core.find(idVendor=0, idProduct=1)

This will return a list of devices, even when the list is empty or contains just one element. Set up the USB device further and start building your first packet to send. Before blindly sending commands to the charger, what would be the most destructive - errr - non destructive command: getting device information.

Do some reads on the device without sending anything to it. Eventually you will receive a packet.

In this scenario the packets have a common format for receiving and sending. You might notice a 0x0f at the beginning of each packet. As dotPeek is unable to tell you where it comes from and where it is designed, I am going to spoil it for you.

In file FormLoader.cs we find the following definition:

201 public const byte UART_DATA_START = 15;

The UART [6] we are basically telling the charger we are coming over USB. There is also a mobile application to send commands via Bluetooth, but I haven't done this one, yet.

There is function called Get_System_Data. When we look at the definition of the function the code is very messy.

/upload/get/5c6955eb_0016.png

Alot [5] of constant values are assigned to variables, which are assigned to variables and then used as index. This looks confusing but the best way is to just begin prototyping it the same way.

num1 = 0
str1 = ''
num2 = 4
# Do not kown this yet
inPacket = raw_packet  # raw_packet is the contents read by pyusb.
index1 = num2
num3 = 1
num4 = index1 + num3
num5 = int(inPacket[index1], 16)  # Python 3: probably bytes as input.
...

And so on. After building your prototyped function you will see parts which can be optimized, but do not care about it too much in the beginning. Try to understand how packets are constructed and what they contain. But for example the num2 = 4 could be removed and replaced with index1 = 4, as num2 is not used after that point.

By breaking the packet down, byte by byte (there are only 64 bytes), we then try to create data structures from it, like the one mentioned in the beginning. Each information gathered so far helps in decoding packets received and to later send packets.

For decoding packets I personally use the Python struct module. By reading the definition of Get_System_Data we define a system class, and machine_info as FormLoader.cs calls it.

With struct we define a data structure which can parse the 64 bytes each packet has and apply it to a named tuple in Python. After reading the original decompiled code, I came up with this definition:

#: Machine response data
MACHINE_INFO_STRUCT = '>3xBBBBBBBBBBBBB6sBBhBBBBbB'
#: Tuple for device information
MachineInfo = namedtuple('machine_info',
                         ['Sys_Mem1', 'Sys_Mem2', 'Sys_Mem3', 'Sys_Mem4',
                          'Sys_Advance', 'Sys_tUnit', 'Sys_Buzzer_Tone',
                          'Sys_Life_Hide', 'Sys_LiHv_Hide',
                          'Sys_Eneloop_Hide', 'Sys_NiZn_Hide', 'Sys_Lcd_time',
                          'Sys_Min_Input', 'core_type', 'upgrade_type',
                          'is_encrypted', 'customer_id', 'language_id',
                          'software_version_hi', 'software_version_lo',
                          'hardware_version', 'reserved', 'checksum',
                          'software_version', 'machine_id'])

The struct definition MACHINE_INFO_STRUCT describes how each byte of the packet should be interpreted. In words:

  • We decode it as big-endian.
  • Ignore 3 bytes as these are protocol commands.
  • Read 14 unsigned bytes (0..255), each into a separate variable.
  • Read 6 characters or a string of length 6.
  • Read 2 individual bytes.
  • Read a short (2 bytes).
  • Read 4 individual unsigned bytes.
  • Read a signed byte (-128..127).
  • Read a unsigned byte.

The MachineInfo is a namedtuple, to make it very easy to assign and access values. When we receive a packet and we have determined the type, we can do something like this:

data = unpack(MACHINE_INFO_STRUCT, response[:32])
machine = MachineInfo(\*data, 0, machine_id)

Sending Data

While reading data is one side, we also need send commands. When optimizing the code the private bool Send_USB_CMD(int Solt, byte CMD) function can be annoying, but refactoring the prototype code will very quickly tell you where to place your bytes.

Whilst the original code is hiding the CMD parameter position behind some index calculations (which lies in nature of decompilation) we can translate the following code:

/upload/get/5c696a02_0017.png

To a single byte-string if we use the Get_System_Data CMD code 95:

\x0f\x00\x5a\x00

One really annoying thing is the index counting. The program starts filling the outPacket at offset 1. Which is actually 0, which is always set to 0x0f. It is protocol definition.

Tricky thing is the real offset 1. It has to be set to a specific value. To find out which one, we have to further investigate the code. This changes depending on the operation you want to call.

/upload/get/5c697a06_giphy-downsized-3.gif

Going further through the code, we might find a location where it sets the offset 1 to a other valua than 0. Eventually the offset becomes 4. The command so far is now

\x0f\x03\x5a\x00

Sending this to the device returns no result, therefore we are still missing something. Somewhere was a loop adding up all bytes of packet. This could be a checksum and/or the command is still incomplete. Let's look at the Send_USB_CMD again. When working through the code, it is help full to take notes.

I have removed a switch-case statement for your convenience.

/upload/get/5c6ad07d_0021.png

After working through the code, taking notes. the resulting packet for CMD=95, Solt=0 [7] should look like this:

\x00\x0f\x03\x5a\x00\x00\x5d\xff\xff

The two bytes of \xff (255) at the end define the end a packet. Every packet is produced after this schema.

Byte Description
1 It is always 0, you will learn soon enough why! ARRGHGGHG
2 Start of message (Always 0x0f (15)).
3 Calculate the value based on the index.
4 The command op code.
5 It is 0.
6 The slot index (0 to 3 (4 Slots)).
7 The sum of the variable data (Byte 3 to 6)
8 Is always 0xff (255)
9 Is always 0xff (255)

Sending this to the device is still not correct, why? To find out why, delving deeper into the nested classes we find an abomination. The decompiled code for SpecifiedOutputReport.cs in class UsbLibrary.SpecifiedOutputReport, there is this one function:

16 public bool SendData(byte[] data)
17 {
18   byte[] buffer = this.Buffer;
19   for (int index = 1; index < buffer.Length; ++index)
20     buffer[index] = data[index];
21   return true;
22 }

The line 19 defines a loop starting at index 1...

/upload/get/5c6acde2_array_retarded.png

With all this knowledge collected the final valid packet to send to your device is:

\x0f\x03\x5a\x00\x00\x5d\xff\xff

That's it. We have done it.

KTHXBYE!

[1]BTW, giving descriptive names for your variables is totally over rated.
[2]Bob, is it you? [3]
[3]Stupidest joke so far. He is no constructor, he is a builder.
[4]As you might have noticed. I am just reading and translating the code.
[5]alot this was an intentional typo.
[6]Universal Asynchronous Receiver/Transmitter
[7]Solt [SIC]

[Teaser] How to reverse engineer communication protocols of embedded devices

by jpk on 2019-02-16 12:25:16 edited on 2019-02-16 12:28:02

Sneak Preview

/upload/get/5c56b68e_doge.gif

These letters. Such announcement. Many words.

In the next few days I will publish two - not one - but two articles on how to approach a problem on how to reverse engineer protocols. There have been to applications I looked into to code a library for my home uses.

#1 - MC3000 Charger

This charger by SkyRC provides an USB and Bluetooth (BT) interface (Spoiler: I am not covering the BT interface. Not yet). The USB interface is used to update the firmware and to program and interact with the charger during charging.

The Windows software provided by SkyRC can program each slot individually to support different types of batteries with different charging capacities.

As a result of my analysis, and this will be one of the upcoming articles, I reversed the application and wrote a Python library. To do so I dissected a .NET application. So no big magic here!

#2 - LW12 WiFi LED Controller

This was a tricky one. It is a low budget Chinese WiFi LED controlled with a mobile app. The Android app I looked at was encrypted using a separate VM layer on-top of the Dalvik engine. (Spoiler: No need to reverse this, and I did not do it.)

Sometimes there are simpler solutions. This is what the second article will be about.

The controller itself comes by many names: Foxnovo and I remember buying it as a Lagute.

KTHXBYE.

Hack back during #cyberwar

by jpk on 2019-02-13 07:19:16 edited on 2019-02-13 18:06:31

According this link people want us to hack back, and the government is like

/upload/get/5c63c452_giphy.gif

KTHXBYE.

The GIF is probably copyrighted material by the The Fine Brothers. Plz Don't sue me. I no make cyber moneyz with this blog.

You sneaky little non-goat. Razer SoftMiner - Crypto Mining

by jpk on 2019-02-05 17:45:05

I confess! I am a huge LED fan boy and Razer has great stuff and they are really create in offering you a crypto miner to earn points for their fantastic virtual silver currency.

They collect some Ethereum and Monero and you will get SILVER, which can get you a discount in their shop... hell no!

Hm... I might check it out another day.

KTHXBYE.

Welcome to the farm!

by jpk on 2019-02-05 17:17:22

This magnificent piece of blog is now available under "https://goatpr0n.farm/". Marvelous!

KTHXBYE.

Is there a RSS feed, yet?

by jpk on 2019-02-02 20:23:11

No.

Can I haz ur dataz // remote data acquisition over ssh

by jpk on 2019-02-01 08:55:31 edited on 2019-02-11 07:14:57

Remote data acquisitions over ssh

To get your data trough ssh to your local storage, you simply use pipes. It does not matter if you use cat, dd or any other command line tool which outputs the data on standard output (stdout).

Using cat

When using cat the there is no need to add any additional parameters in your command chain. A simple cat <input> will suffice.

Cat does not have any progress information during read operations.

Using dd

The programm dd requires more user interaction during the setup phase. To use dd you have to give the if=<input> argument. The use of different blocksizes (bs) will not have that much of an inpact on the speed.

Feel free to have a look at this article.

/upload/get/5c53fe16_doge.jpeg

Wow. So scientific! Much CLI.

Example

A simple example with no output to the terminal except of errors. The output to stderr is not surpressed.

$ # Using cat to copy /dev/sda
$ ssh <user@remotehost> 'cat /dev/sda' > sda.raw

If you want to surpress errors, use:

$ # Using cat to copy /dev/sda
$ ssh <user@remotehost> 'cat /dev/sda 2>/dev/null' > sda.raw

The next example will demonstrate the use of dd.

$ # Using dd to copy /dev/sda
$ ssh <user@remotehost> 'dd if=/dev/sda' > sda.raw

Of course you can surpress errors as well.

Speeding up the data acquisition and minor tweaks

With the basics covered, we can begin optimizing our data transfer. In the first step we speed up the transfer with gzip.

The argument -c will write the compressed data to stdout which will be piped to your local system.

Of course you can use this with cat as well.

$ ssh <user@remotehost> 'dd if=/dev/sda | gzip -c' | gunzip > sda.raw

To add some progress information, you have two options with dd.

$ # dd status
$ ssh <user@remotehost> 'dd if=/dev/sda status=progress | gzip -c' \
> | gunzip > sda.raw

The status argument writes the output to stderr and will not end up in your local copy.

$ # dd through pv
$ ssh <user@remotehost> 'dd if=/dev/sda | gzip -c' \
> | pv | gunzip > sda.raw

Pv needs to be installed separatly. Check your packet manager. 0r teh Googlez!

Update #01

Fixed a problem with in the examples. Had a pipe too much. #Fail

KTHXBYE.

Fixed the order of my posts

by jpk on 2019-01-30 11:36:21 edited on 2019-01-31 18:07:14

The posts will be sorted now. I have merged my testing/refactoring branch to do so. There might be some bugs now...

KTHXBYE

What if the cult of dd is right?

by jpk on 2019-01-23 11:47:26

Are you a believer?

There are articles out there talking about the useless usage of dd and why cat is better. Cat is faster because it automatically adjusts the blocksize and dd is slow because it internally only works with 512 byte blocks. This and That.

I did some simple tests with time, dd and cat, added some obscure parameters to dd, because cat is better.

Testing dd with status and specific blocksize

$ time dd if=/dev/sdd of=test.dd status=progress bs=8M
15921577984 bytes (16 GB, 15 GiB) copied, 878 s, 18.1 MB/s
1899+1 records in
1899+1 records out
15931539456 bytes (16 GB, 15 GiB) copied, 879.018 s, 18.1 MB/s

0.04s user 23.33s system 2% cpu 14:39.03 total

Testing dd

$ dd if=/dev/sdd of=test.dd
31116288+0 records in
31116288+0 records out
15931539456 bytes (16 GB, 15 GiB) copied, 869.783 s, 18.3 MB/s
16.13s user 159.22s system 20% cpu 14:29.80 total

Testing cat with pv

$ time cat /dev/sdd | pv > test.raw
14.8GiB 0:14:43 [17.2MiB/s] [        <=>                            ]
0.28s user 25.84s system 2% cpu 14:43.18 total

Testing cat

$ time cat /dev/sdd > test.raw
0.18s user 21.21s system 2% cpu 14:42.25 total

Y U DO IT WRONG

Somehow my cat is not as fast as dd.

KTHBYE

There is not enough cyber in the world

by jpk on 2019-01-21 14:21:02

My recent favuorite hash tags in social networks are:

  • cyberwar / cyberkrieg
  • cold-cyberwar / kalter cyberkrieg

KTHXBYE

Sort

by jpk on 2019-01-16 09:19:46

BTW: When you write your own flat file based blog software, ensure to sort the contents before displaying. The correct order of posts is not guaranteed

The MQTT Broker Misquitto has some problems with unauthenticated clients

by jpk on 2019-01-16 09:19:46

I use Mosquitto as my main MQTT broker for my smart home environment. Recently I came across some behaviour I should investigate further in near future.

The MQTT clients did not authenticate to broker with a correct password. But the clients were accepted, at least for a short period of time. After that the clients were disconnected. During this short time, the devices could receive and sent messages without being authenticated. hm.....

Flask Recipe: Jinja2 Template Filters

by jpk on 2018-12-24 09:37:44

Flask Recipe: Jinja2 Template Filters

This post covers a simple Python recipe to programmatically make all filters available to your templates in your flask app.

The code utils/filters.py contains a function called init_app(), when it is called it will look through all globally accessible objects. When a function name starts with a filter_ it will be added as template filter for you to use in the Jinja2 templates.

 1 from datetime import datetime
 2 
 3 
 4 def filter_utcfromtimestamp(timestamp):
 5     return datetime.utcfromtimestamp(int(timestamp))
 6 
 7 
 8 def filter_caps(text):
 9     return text.upper()
10 
11 
12 def filter_foo(text):
13     return text.replace('foo', 'bar')
14 
15 
16 def init_app(app):
17     for func in globals():
18         if func.startswith('filter_'):
19             name = func.split('_')[-1]
20             app.add_template_filter(globals()[func], name)

The above code defines three filters which will be avaiable after intitialization:

  • utcfromtimestamp: converts a unix timestamp to a human readable format. Sample: {{ 398044800|utcfromtimestamp }} outputs 2018-08-13 00:00:00
  • caps: convert a string into upper case. Sample: {{ 'foo'|caps }} outputs FOO
  • foo: replace foo with bar. Sample: {{ 'foobar'|foo }} outputs barbar

To intitialize and register the filters you can call the function as shown in the sample create_app() function below.

 1 from flask import Flask
 2 # [...]
 3 def create_app(test_config=None):
 4     app = Flask(__name__)
 5     # [...]
 6     from .utils import filters
 7     filters.init_app(app)
 8     # [...]
 9     return app