共用方式為


DIY: Bitcoin-монитор из Nokia 5110, Netduino и Azure

Это перевод оригинальной статьи Netduino and Windows Phone Bitcoin tracker on Azure

Моего интерна и меня попросили выступить в Дурбанском технологическом университете перед студентами третьего курса для того чтобы вдохновить их возможностями использования Netduino и/или Windows Phone в их проектах.

Нам хотелось показать не просто мигающий светодиод, но что-нибудь что будет иметь отношение к реальным живым сценариям. И мы решили продемонстрировать это:

Просим прощения за ужасный GIF. Вы можете назвать это трекером изменения цены на биткоин. Граф отражает изменение, а светодиод меняет цвет на зеленый при росте цены и на красный, когда цена падает. (На экране вы можете обнаружить опечатку – вместо USD должно выводиться BTC).

WP_20140213_10_14_46_Pro - Copy

Azure

Первым очевидным шагом для нас является определение места откуда будут поступать данные. Один из простых и легких для использования API поставляется mtgox (примечание: в связи с разными рода противоречивыми дискуссиями вокруг них, может иметь смысл использовать что-то вроде Bitstamp). Проблема в том, что вместе с нужными нам данными о цене, источник выдает множество других данных, которые будет слишком затратно загружать и обрабатывать на Netduino. Решение состоит в том чтобы создать промежуточный сервис между устройством и API, который вырезает ненужные данные. И сделать это с помощью Azure ДЕЙСТВИТЕЛЬНО легко.

Создадим новый веб-сайт на портале Azure:

image

Укажите любой URL по желанию и запустите создание сайта, перейдя затем в Visual Studio. Создайте новое веб-приложение ASP.NET MVC:

image

Мы собираемся загрузить последние данные о цене из mtgox API и отправлять сокращенные данные о цене. Для этого нам необходимо десериализовать JSON-данные в .NET-объекты. Обратитесь к API и скопируйте JSON-результат. Затем создайте новый .cs-файл в папке Models под названием MtGoxTicker.cs. Уберите определение класса по умолчанию и с помощью команды Edit > Paste Special > Paste JSON as classes вставьте скопированный текст. Целая пачка классов, которая отображает структуру API, будет создана автоматически. Переименуйте объект RootObject в MtGoxTicker.

    public class MtGoxTicker
    {
        public string result { get; set; }
        public Data data { get; set; }
    }
 
    public class Data
    {
        public High high { get; set; }
        public Low low { get; set; }
        public Avg avg { get; set; }
        public Vwap vwap { get; set; }
        public Vol vol { get; set; }
        public Last_Local last_local { get; set; }
        public Last_Orig last_orig { get; set; }
        public Last_All last_all { get; set; }
        public Last last { get; set; }
        public Buy buy { get; set; }
        public Sell sell { get; set; }
        public string item { get; set; }
        public string now { get; set; }
    }
 
    public class High
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Low
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Avg
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Vwap
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Vol
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Last_Local
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Last_Orig
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Last_All
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Last
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Buy
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }
 
    public class Sell
    {
        public string value { get; set; }
        public string value_int { get; set; }
        public string display { get; set; }
        public string display_short { get; set; }
        public string currency { get; set; }
    }

В папке Controllers создайте новый WebAPI-контроллер или используйте один из существующих. Вы можете удалить все методы, кроме Get. Все что потребуется от этого метода – это загрузить последние данные о ценах из API и вернуть одно значение BUY, которое нам требуется.

Измените тип возвращаемых данных Get на double, затем приведите метод в следующий вид:

  public double Get()
        {
            try
            {
                var wc = new WebClient();
                var response = JsonConvert.DeserializeObject<MtGoxTicker>(wc.DownloadString("https://data.mtgox.com/api/2/BTCUSD/money/ticker"));
                if (response != null)
                {
                    return double.Parse(response.data.buy.value);
                }
            }
            catch (Exception)
            {
 
            }
 
            return -1;
        } 

Проще говоря, когда кто-то вызывает метод Get, он загружает цены из API, десериализует их и возвращает только необходимые нам данные.

Последнее что нам нужно сделать - это заставить наш API всегда возвращать ответ в JSON. Перейдите к последней строчке метода Appication_Start в Global.asax.cs и добавьте следующий код после существующих строчек:

GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Add(new JsonMediaTypeFormatter()); 

К этому моменту ваш веб-сайт в Azure уже должен был создаться, так что мы можем двигаться дальше и разместить сайт прямо из Visual Studio. Самый легкий способ сделать это – кликнуть правой кнопкой на проекте в Solution Explorer и выбрать команду Publsh. Затем нажмите Import и пройдите процесс логина и имопрта деталей вашей Azure-подписки (этот процесс выглядит значительно приятнее в Visual Studio 2013). Публикация вашего сайта может занять минуту или две,  после чего вы сможете перейти на него.

Для того чтобы протестировать то, как должен выглядеть ваш сайт, вы можете перейти на мой https://bitcoinpusher.azurewebsites.net/api/Price (примечание: я не могу гарантировать, что эта ссылка будет работать вечно).

Hardware

- Netduino Plus (или Plus 2)

- Nokia 5110 LCD ($4 на портале DX)

- RGB LED – мы использовали SMD 5050, но подойдет любой RGB LED.

Netduino

Netduino будет загружать последние данные о цене из сервиса в Azure, отображать их и рисовать график. Этот процесс будет повторяться периодически в бесконечном цикле и будет обновлять данные так быстро, как сможет (будет зависеть от скорости интернета).

Чтобы писать на экран Nokia 5110 LCD мы будем использовать библиотеку, разработанную членом сообщества Netduino, про которую вы можете прочитать по этой ссылке. Имейте в виду, что в алгоритме рисования линии есть ошибка, так что вам может потребоваться загрузить исходные коды (в конце этой статьи) для того чтобы использовать мою исправленную версию.

Для упрощения сетевых вызовов мы можем использовать HTTP-клиент из инструментов .NET MF Toolbox.

Вместо описания кода строчка за строчкой, я публикую весь код Program.cs с комментариями. Он достаточно прост для понимания:

    public class Program
    {
        //the red and green pins of the RGB LED
        private static OutputPort _redPin = new OutputPort(Pins.GPIO_PIN_D1, false);
        private static OutputPort _greenPin = new OutputPort(Pins.GPIO_PIN_D0, false);
 
        public static void Main()
        {
 
            //setup the LCD with appropriate pins.
            var lcd = new Nokia_5110(true, Pins.GPIO_PIN_D10, Pins.GPIO_PIN_D9, Pins.GPIO_PIN_D7, Pins.GPIO_PIN_D8)
            {
                BacklightBrightness = 100
            };
 
            //create these to store values
            var history = new Queue();
            double lastValue = 0;
            var nextUpdateTime = DateTime.MinValue;
            while (true)
            {
                try
                {
                    //download the price from our API
                    var WebSession = new HTTP_Client(new IntegratedSocket("bitcoinpusher.azurewebsites.net", 80));
                    var response = WebSession.Get("/api/price/");
                    if (response.ResponseCode == 200)
                    {
                        //convert the price to a double from a string
                        var result = double.Parse(response.ResponseBody);
 
                        //if the value went up, change the LED to green, if it went down change to red
                        if (result > lastValue)
                        {
                            _greenPin.Write(true);
                            _redPin.Write(false);
                        }
                        else if (result < lastValue)
                        {
                            _greenPin.Write(false);
                            _redPin.Write(true);
                        }
                        //store this value so we can compare it to the next one
                        lastValue = result;
 
                        //only add points to the graph every x seconds, else it will barely move
                        if (DateTime.Now > nextUpdateTime)
                        {
                            history.Enqueue(result);
                            //store a max of 80 data points as each point will take up 1 pixel, and the screen is
                            //only 80 wide
                            if (history.Count > 80)
                            {
                                history.Dequeue();
                            }
                            //store a value of what time we should add the next data point to the list
                            nextUpdateTime = DateTime.Now.AddSeconds(15);
                        }
 
                        var high = 0d;
                        var low = double.MaxValue;
                        //find the max and min value to determine our range (for the graph).
                        //The reason for this is so that the min value will be the very bottom of the graph, and
                        //the max value will be the very top of the graph regardless of what the values are
                        foreach (double item in history)
                        {
                            if (item < low)
                            {
                                low = item;
                            }
                            if (item > high)
                            {
                                high = item;
                            }
                        }
                        if (high == low)
                        {
                            //if all numbers are the same, artificially seperate high and low so that the
                            //graph will draw in the middle of the screen. Without doing this the
                            //point will be at the very top.
                            high--;
                            low++;
                        }
                        double diff = high - low;
                        lcd.Clear();
                        short x = 1;
                        short prevY = -1;
                        //this loop draws a line from the previous point to the current point, which makes the graph
                        foreach (double item in history)
                        {
                            //work out the y value based on the min/max range, and the available height.
                            //We have 39 pixels height to work with, and shift it by 9 at the end so it doesn't
                            //overlap the text at the top
                            var thisY = (short)((39 - (((item - low) / diff) * 39)) + 9);
 
                            if (prevY != -1) //don't draw from 0,0
                            {
                                //draw a line from the previous point to this point
                                lcd.DrawLine((short)(x - 1), prevY, x, thisY, true);
                            }
                            //remember this pos so we can draw a line from it in the next iteration
                            prevY = thisY;
                            x++;
                        }
                        //Refresh pushes all DrawLine/Rect/Point calls to the screen
                        //Note that this does not apply to writing text, which pushes instantly
                        lcd.Refresh();
 
                        //there is no Math.Round(double,int) so use ToString to get 4 decimals
                        lcd.WriteText("$ " + result.ToString("f4") + " / BTC");
                    }
                    else
                    {
                        FlickerLEDForFaliure();
                    }
                }
                catch (Exception)
                {
                    FlickerLEDForFaliure();
                }
            }
        }
 
        private static void FlickerLEDForFaliure()
        {
            //if the downlaod of the new value fails then flicker the LED. After flickering turn the LED off
            for (int i = 0; i < 30; i++)
            {
                _redPin.Write(true);
                Thread.Sleep(100);
                _greenPin.Write(true);
                Thread.Sleep(100);
                _redPin.Write(false);
                Thread.Sleep(100);
                _greenPin.Write(false);
            }
        }
 
    } 

Ниже представлена схема соединений:

BitcoinPusher_bb

WP_20140128_16_08_32_Pro - Copy

Я оставил подключенной эту схему на ночь и она отработала нормально. Отсоединение от сети и обратное подключение тоже обрабатывается нормально.

Windows Phone

Приложение Windows Phone делает ровно то же самое что и сам Netduino: загружает цену, отображает ее и затем рисует график.

UI – это просто TextBlock для отображения цены и Grid, который будет содержать линии.

wp_ss_20140202_0001

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
 
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="price" x:Name="PriceTextBlock" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle2Style}" FontSize="40"/>
        </StackPanel>
 
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="0">
 
        </Grid>
    </Grid> 

Ниже вы найдете код. Обратите внимание, что так как код полностью основан на коде для Netduino, я не стал его подробно комментировать. Обратитесь к коду для Netduino, если вам что-то не понятно.

    public partial class MainPage : PhoneApplicationPage
    {
        private Queue<double> _history = new Queue<double>();
        private double _lastValue = 0;
        private DateTime _nextUpdateTime = DateTime.MinValue;
        public MainPage()
        {
            InitializeComponent();
            DownloadAndPlotPrice();
        }
 
        private async void DownloadAndPlotPrice()
        {
            try
            {
                var WebSession = new HttpClient();
                var response = await WebSession.GetAsync("https://bitcoinpusher.azurewebsites.net/api/price/");
                if (response.IsSuccessStatusCode)
                {
                    var result = double.Parse(await response.Content.ReadAsStringAsync());
 
                    //if the value went up, change the back to green, if it went down change to red
                    if (result > _lastValue)
                    {
                        LayoutRoot.Background = new SolidColorBrush(Colors.Green);
                    }
                    else if (result < _lastValue)
                    {
                        LayoutRoot.Background = new SolidColorBrush(Colors.Red);
                    }
                    _lastValue = result;
 
                    //only add points to the graph every x seconds, else it will barely move
                    if (DateTime.Now > _nextUpdateTime)
                    {
                        _history.Enqueue(result);
                        if (_history.Count > 400)
                        {
                            _history.Dequeue();
                        }
                        _nextUpdateTime = DateTime.Now.AddSeconds(4);
                    }
 
                    var high = 0d;
                    var low = double.MaxValue;
                    //find the max and min value to determine our range (for the graph)
                    foreach (double item in _history)
                    {
                        if (item < low)
                        {
                            low = item;
                        }
                        if (item > high)
                        {
                            high = item;
                        }
                    }
                    if (high == low)
                    {
                        //if all numbers are the same, artificially seperate high and low so that the
                        //graph will draw in the middle of the screen
                        high--;
                        low++;
                    }
                    double diff = high - low;
                    //remove all previous lines in preperation for redrawing them
                    ContentPanel.Children.Clear();
                    short x = 1;
                    short prevY = -1;
                    foreach (double item in _history)
                    {
                        //we now have 300 pixels of vertical space to play with
                        var thisY = (short)(300 - (((item - low) / diff) * 300));
 
                        if (prevY != -1) //don't draw from 0,0
                        {
                            //draw a line from the previous point to this point
                            //Line is a XAML control that we use to display lines
                            ContentPanel.Children.Add(new Line
                            {
                                X1 = (x - 1),
                                Y1 = prevY,
                                X2 = x,
                                Y2 = thisY,
                                StrokeThickness = 4,
                                Stroke = new SolidColorBrush(Colors.White)
                            });
                        }
                        prevY = thisY;
                        x++;
                    }
 
                   PriceTextBlock.Text = ("$ " + result.ToString("f5") + " / BTC");
                }
                else
                {
                    ShowFaliureFaliure();
                }
            }
            catch (Exception)
            {
                ShowFaliureFaliure();
            }
 
            DownloadAndPlotPrice();
        }
 
        private async void ShowFaliureFaliure()
        {
            //if the download of the new value fails then flicker the background.
            for (int i = 0; i < 30; i++)
            {
                LayoutRoot.Background = new SolidColorBrush(Colors.Orange);
                await Task.Delay(100);
                LayoutRoot.Background = new SolidColorBrush(Colors.Black);
                await Task.Delay(100);
            }
        }
    } 

На этом все. Дайте мне знать, если у вас будут вопросы или проблемы в комментариях (оригинальной статьи – прим. перев.) или на твиттере.

Вы можете загрузить исходные файлы отсюда!

Это перевод оригинальной статьи Netduino and Windows Phone Bitcoin tracker on Azure

Дополнительные ссылки