「技術」カテゴリーアーカイブ

【ラズパイ】【いろいろ計測モニター】Windows側からも操作しちゃう

前回までの状況はこちら。

最新ソースはこちら。

ラズパイ https://github.com/takishita2nd/RaspiDisplayMonitor

Windows https://github.com/takishita2nd/IroiroMonitor

Windows側からラズパイの情報を取得できるなら、

Windows側からでもラズパイを操作する事も出来ます。

なので、Windows側からスイッチ操作を行う処理を作ってみます。

Windowsにボタンを設置。

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="時刻" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10" Width="239" Height="86" FontSize="48"/>
        <Label Grid.Row="1" Grid.Column="0" Content="温度" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10" Width="239" Height="86" FontSize="48"/>
        <Label Grid.Row="2" Grid.Column="0" Content="湿度" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48" Margin="10" Width="239" Height="87"/>
        <Label Grid.Row="3" Grid.Column="0" Content="CPU温度" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48" Margin="10" Width="239" Height="87"/>
        <Label Grid.Row="4" Grid.Column="0" Content="GPU温度" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48" Margin="10" Width="239" Height="87"/>
        <Label Grid.Row="0" Grid.Column="1" Content="{Binding DateTime}"  HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24"/>
        <Label Grid.Row="1" Grid.Column="1" Content="{Binding Temperature}"  HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
        <Label Grid.Row="2" Grid.Column="1" Content="{Binding Humidity}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
        <Label Grid.Row="3" Grid.Column="1" Content="{Binding CpuTemp}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
        <Label Grid.Row="4" Grid.Column="1" Content="{Binding GpuTemp}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48"/>
        <Button Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="2" Content="ボタン" Command="{Binding ButtonClickCommand}" FontSize="48" />
    </Grid>

ボタンを押すと、ラズパイ側にPOSTリクエストを送信するようにします。

今後の拡張性を考えて、コマンド番号みたいなものを送れるようにしましょうか。

    [JsonObject("CommandModel")]
    class Command
    {
        [JsonProperty("number")]
        public int number { get; set; }
    }
    public class MainWindowViewModel : BindableBase
    {
        private const int CommandSwitch = 1;

        public DelegateCommand ButtonClickCommand { get; }

        public MainWindowViewModel()
        {
            ButtonClickCommand = new DelegateCommand(async () =>
            {
                Command cmd = new Command();
                cmd.number = CommandSwitch;
                var json = JsonConvert.SerializeObject(cmd);
                try
                {
                    HttpClient client = new HttpClient();
                    var content = new StringContent(json, Encoding.UTF8);
                    await client.PostAsync("http://192.168.1.15:8000/", content);
                }
                catch (Exception ex)
                {
                }
            });

これで、ラズパイ側にPOSTリクエストを送れるようになりました。

次はラズパイ側のコードを書いていきます。

class StubHttpRequestHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_len = int(self.headers.get('content-length'))
        requestBody = json.loads(self.rfile.read(content_len).decode('utf-8'))

        if requestBody['number'] == 1:
            lock.acquire()
            GLCD.GLCDDisplayClear()
            lock.release()
            pushButton()

        response = { 'status' : 200 }
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        responseBody = json.dumps(response)

        self.wfile.write(responseBody.encode('utf-8'))

HttpRequestHandlerクラスにdo_POSTメソッドをオーバーライドします。

これを実装することで、POSTリクエストを受診して処理することが出来ます。

受信したデータがボタン操作ならば、ラズパイ側で行っているボタン操作と同じ処理をおこないます。

def pushButton():
    global mode
    mode += 1
    if mode > 4:
        mode = 1

しかし、ここで考えなければならないのは、ラズパイ側の周期処理とHTTPサーバ処理は非同期処理を行っていると言うこと。

はい、処理が競合しちゃいます。

なので、スレッド間の待ち合わせ処理を行う必要があります。

方法はいろいろあるのですが、今回は一番簡単な方法を使用します。

Lockを使用する方法です。

lock = threading.Lock()
    try:
        while True:
            lock.acquire()
            Humidity = AM2320.GetHum()
            Temperature = AM2320.GetTemp()

            if sw == True:
                GLCD.GLCDDisplayClear()
                pushButton()
                sw = False

            if mode == 1:

-中略-

            lock.release()
            time.sleep(1)
    except KeyboardInterrupt:
        GLCD.GLCDDisplayClear()
        GPIO.cleanup()

lock = threading.Lock()を定義し、同じlockで周期処理全体と、HTTPのスイッチ処理をlock/releaseで囲みました。

これで、一方がlockされている場合、もう一方はlockがreleaseされるまで処理に待ったがかかります。

これを使用すれば、ラズパイの遠隔操作も可能になります。

【ダイエット支援】【食事管理】グラフの値をパーセンテージにして、適正量かどうかを見えるようにする。

前回までの状況はこちら

前回のままだと、摂取量をそのまま数字としてグラフに表示させているだけなので、

この数字が適正量なのかを判断するために、適正値との割合をパーセンテージにして表示させたいと思います。

といっても、どれくらいが適正量なのか、というのが、体型などに左右されやすいものなので、

こちらの記事の計算式を参考にして、

体重60kg、摂取カロリー2000kcalとして計算すると、

  • タンパク質120g(体重×2g、1g=4kcal)
  • 脂質55.5g(総カロリーの25%、1g=9kcal)
  • 炭水化物255g(総カロリーからタンパク質と脂質を除いた残り、1g=4kcal)

この数字を直に与えました。

            baseData: {
                protein: 120.0,
                liqid: 55.5,
                carbo: 255.0,
                calorie: 2000.0
            },

これ、ちゃんとした条件をもとに計算する仕組みが必要だね。

                    self.datasets.push(response.data.data.protein / self.baseData.protein * 100);
                    self.datasets.push(response.data.data.liqid / self.baseData.liqid * 100);
                    self.datasets.push(response.data.data.carbo / self.baseData.carbo * 100);
                    self.datasets.push(response.data.data.calorie / self.baseData.calorie * 100);

これでグラフがパーセンテージになります。

なので、どの栄養素が足りてなくて、どの栄養素を取りすぎているかがわかります。

【ANDROID】【実質北海道一周】移動距離に応じて表示を変える

前回までの状況はこちら。

簡単ですが、UIは作ったので、実際に移動距離から都市名を表示させる処理を作っていきます。

class StartEnd(startCity: String, endCity: String) {
    val startCityName : String = startCity
    val endCityName : String = endCity
}
class AroundHokkaido {

    fun getCity() : StartEnd {
        var tempDistance = 0.0
        var start : String = ""
        var end : String = ""
        var loop : Boolean = false
        run {
            citylist.cityList.forEach{
                if(loop){
                    end = it.city
                    return@run
                }else{
                    tempDistance += it.distance
                    if(totalDistance < tempDistance){
                        start = it.city
                        loop = true
                    }
                }
            }
        }
        return StartEnd(start, end)
    }

Jsonで作成した都市と距離のデータから、移動距離と都市間距離を比較しながら加算していき、都市間距離合計が移動距離を超えたときにそのときの都市を返します。

forEach()を使っているのですが、forEachは処理じゃなくて関数なので、途中で処理を抜けるには、breakではなくreturnになります。

それをrunというラベルを指定してreturnすることで、run{}を抜けるところまでジャンプできます。

ややこしい。

でも大分Kotlinにも慣れてきた気がする。

class MainActivity : AppCompatActivity() {

    private fun updateCitydistance(startEnd : StartEnd){
        startCity.text = startEnd.startCityName
        endCity.text = startEnd.endCityName
    }

kotlinはUIにアクセスするの楽だからいいね。

GPSって、思った以上にいい加減、というか、誤差が大きくて、動かしていなくても、1mぐらい移動したものと見なされるみたいです。

後々対処しないとだけど、今は、移動するのがめんどくさいので、このままにしておきます。

確認用に、Jsonを修正。

うん、きちんと変わりましたね。

修正しなきゃいけないところはあるけど、とりあえずはこれでいいか。

COCOS2D-Xの開発環境を作成してWindowsで動かすまでの話

こちらの話の続き。

ついでだからWindowsで動かすところまでやりました。

Android端末が入らない分、デバッグが楽になるかもしれないので。

必要なのはVisual Studio 2017。

2019ではダメです。

ツールの機能取得で、以下の機能をインストールします。

たぶん、必要なのはこれだけだと思う。

cocos run -p win32

で動きました。

Visual Studio 2017のRUNでも動くよ。

Cocos2d-xの開発環境を作成してAndroidで動かすまでの話

一応、プロジェクトを作成して、Hello Worldが出るまでやりました。

手順などは、Qiitaにまとめましたが、

https://qiita.com/takishita2nd/items/0b54af9860f54c65fd24

実際はもっと手こずったので、いろいろと愚痴を書きたい。

まず、Python2.7で無ければセットアップからプロジェクト作成まで動かないのだが、

Python3が動いているのは分かっているのにPython3の本体がどこにあるか分からない、という自体に。

そもそもWindowsにインストールした覚えがない。

ググってプログラムの本体を調べるコマンドを探しまして、

where python

って打てば良いんですけど、

これコマンドプロンプロのコマンドね。

最近はPowerShellをよく使うから。

C:\Users\[ユーザー名]\AppData\Local\Microsoft\WindowsApps\python.exe

にありました。

分かるか。

展開したPython2.7を展開し、環境変数のPATHの設定で、上のフォルダの記載がある場所の上にPython2.7のPATHを書かなければならない。

PATHの検索順をPython2.7→Python3に変えるんですね。

めんどくせぇ。

今時Python2.xなんて使うやついないよ。(たぶん)

もう設定元に戻したわ。

プロジェクト作るときだけ変えれば良い。

そして、もう一つ、Cocoa2d-xってWindowsでも動かすことができるんですが、

いわゆるマルチプラットフォームになっていて、C++の共通コードだけ記述すれば、他のOSでも動かすことができる。

しかし、Windowsで動かす場合は

Visual Studio 2017が必要。

2019ではダメらしい。

めんどくさい。

(一応動かしたけど、後でまとめるわ)

でも、動いたので、あとはガリガリC++のコードを書いていけば。

IDE何使えば良いんだ?

【openTK】STLファイルを表示させたい。

以前取り上げたopenTKのやつ。

これを使ってSTLファイルを表示する、というのをやってみたいと思います。

使用するSTLファイルは、こちらからダウンロードしました。

https://www.3dagogo.com/creativetools/designs/3DBenchy

ただしく処理できれば、こんな風に表示されるはずです。

STLファイルの読み込みはこちらのサイトを参考にしました。

https://codingsquare.net/cs/stlfile/#toc11

STLのデータは法線ベクトルと、三角形を構成する頂点の座標を示す3つのベクトルで構成されています。

法線ベクトルは三角形の面の表側を向いている方向を示すベクトルです。

		public bool ReadBinary(string filePath)
		{
			// filePath が null か、ファイルが存在しない場合はエラーとする
			if (filePath == null || File.Exists(filePath) == false)
				return false;

			try
			{
				// バイナリファイルの読み込み
				using (var reader = new BinaryReader(new FileStream(filePath, FileMode.Open, FileAccess.Read)))
				{
					// ヘッダ読み込み
					Header = reader.ReadBytes(HeaderLength);

					// ファセットの枚数読み込み
					uint size = reader.ReadUInt32();

					// ファイルの残りのバイト数
					long rest = reader.BaseStream.Length - reader.BaseStream.Position;

					// ファセット1枚分のバイト数
					const int FacetLength = 50;

					// ファイルの残りのバイト数が、求められるファセットの枚数分のバイト数より少なければエラー
					if (rest < FacetLength * size)
						return false;

					// 全ファセット読み込み
					Facets = new Facet[size];
					for (int i = 0; i < size; ++i)
					{
						// ファセット1個分のバイト配列読み込み
						byte[] bytes = reader.ReadBytes(FacetLength);

						// ファセットデータ生成と配列への格納
						int index = 0;
						const int offset = sizeof(float);
						Facets[i] = new Facet(
							new Vertex(
								BitConverter.ToSingle(bytes, index),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset))
						);
					}
				}
			}
			catch (Exception)
			{
				return false;
			}
			return true;
		}

これでバイナリのSTLデータを読み込み、描画させます。

        STLFile stlFile = new STLFile();
        public Game() : base(800, 600, GraphicsMode.Default, "0-3:GameWindow")
        {
            stlFile.ReadBinary("3DBenchy.stl");
        }

        //画面描画で実行される。
        protected override void OnRenderFrame(FrameEventArgs e)
        {
            base.OnRenderFrame(e);

            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            GL.MatrixMode(MatrixMode.Modelview);
            Matrix4 modelview = Matrix4.LookAt(Vector3.Zero, Vector3.UnitZ, Vector3.UnitY);
            GL.LoadMatrix(ref modelview);

            int count = 0;
            foreach(var f in stlFile.Facets)
            {
                count++;
                GL.Begin(BeginMode.Triangles);

                GL.Color4(Color4.White);
                GL.Normal3(f.Normal.X, f.Normal.Y, f.Normal.Z);
                GL.Vertex3(f.Vertex1.X / 30, f.Vertex1.Y / 30, f.Vertex1.Z / 30);
                GL.Vertex3(f.Vertex2.X / 30, f.Vertex2.Y / 30, f.Vertex2.Z / 30);
                GL.Vertex3(f.Vertex3.X / 30, f.Vertex3.Y / 30, f.Vertex3.Z / 30);

                GL.End();
            }

            SwapBuffers();
        }

そしてライトの設定も加えます。

こちらのサイトを参考にしました。

https://ameblo.jp/nishi-u6fa4/entry-10864018960.html

        //ウィンドウのサイズが変更された場合に実行される。
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height);
            GL.MatrixMode(MatrixMode.Projection);
            Matrix4 projection = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, (float)Width / (float)Height, 1.0f, 64.0f);
            GL.LoadMatrix(ref projection);
            GL.MatrixMode(MatrixMode.Modelview);

            Matrix4 look = Matrix4.LookAt(3.0f * Vector3.One, Vector3.Zero, Vector3.UnitY);
            GL.LoadMatrix(ref look);
            GL.Enable(EnableCap.Lighting);
            float[] position = new float[] { 1.0f, 2.0f, 3.0f, 0.0f };
            GL.Light(LightName.Light0, LightParameter.Position, position);
            GL.Enable(EnableCap.Light0);
        }

実行結果はこちら。

なんだこりゃ。

たぶん、データは間違ってないと思うんですよ。

問題は視点なのかな、と思います。

視点を変えるとか、変更できるようにすればちゃんと表示させることができるかもしれません。

もうちょっと勉強します。

【北海道大戦】今後の実装について

まぁ、いまのままでもそこそこ楽しめるのですが、

ゲーム性を高めるために、いろいろ実装していこうかと思います。

まずは、今のバトルは完全乱数で発生させた値でのバトルなので、

ゲーム性を高めるために、じゃんけんバトルのようなものにしようかと思います。

そうなると必要なのがAltseedのシーン切り替え機能ですかね。

このシーン切り替え機能を使用するのに、どれだけ回収が必要なのかも考えないと行けません。

その検証を次回やりましょう。

あと、今は防御側勝利時、特に何もメリットがないので、防御側勝利時に1ターン戦力ボーナス・ペナルティを付与しようかなと思っています。

たぶん、そうすることでゲーム性が向上すると思うんですよね。

よし、がんばります。

あ、あと、Altseed2というのがリリースされたみたいですね。

https://altseed.github.io/index.html

【ラズパイ】【いろいろ計測モニター】あれから改造。

とりあえず、少し改造しました。

調べてみると、GPU温度も測定できるみたいなので、それも入れてみました。

ラズパイのGPU温度を確認するコマンドは、

sudo /opt/vc/bin/vcgencmd measure_temp

sudoなので、pythonプログラム実行時もsudoで管理者権限で実行しなければなりませんが、

サービスで実行させているので、基本的に管理者権限で動作しているので、問題無いでしょう。(実際、動いた。)

時刻というのは現在時刻じゃなくて、最後に測定した時刻です。

HTTPでラズパイからデータを取得していますが、間隔を短くすると、ラズパイZeroでもCPU温度が上がってしまうので、少し間隔を空けています。(1分ぐらい)

次どうしようか。

新しい部品があればネタに出来そうだけど、今はお金が無いので。

グラフ化させてみる?

【ダイエット支援】【食事管理】グラフデータ取得処理を作成する。

前回までの状況はこちら

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/diet-mng

ダッシュボードに表示するグラフを作成していきます。

どんあグラフにしようかというと、

こんな感じのレーダーグラフです。

一日分の摂取したタンパク質、脂質、炭水化物などの情報を取得するAPIが必要になります。

まずは、それを作ります。

app/Repository/EatingManagementRepository.php

    /**
     * 一日あたりのデータを取得する
     */
    public function getDaily($user, $date)
    {
        $eatings = $user->EatingManagements()
            ->where(DB::raw('date_format(date, "%Y-%m-%d")'), $date)
            ->get();

        $retDatas = [];
        for($j = 2; $j < count($this->paramNames); $j++) {
            $retDatas[$this->paramNames[$j]] = 0;
        }
        foreach($eatings as $eating) {
            for($j = 2; $j < count($this->paramNames); $j++) {
                $retDatas[$this->paramNames[$j]] += $eating->{$this->paramNames[$j]};
            }
        }
        return $retDatas;
    }

$dateに取得する日付が入ります。

その日付に一致するデータをすべて取得し、それを栄養素毎に集計します。

これをAPIで取得するようにします。

app/Http/Controllers/Eating/ApiController.php

    /**
     * グラフ用データを取得する
     */
    public function graph(Request $request)
    {
        return response()->json(['data' => $this->eatingManagement->getDaily(Auth::user(), $request->contents['date'])]);
    }
routes/web.php

Route::post('api/eating/graph', 'Eating\ApiController@graph');

これをダッシュボードからデータを取得し、グラフにします。

resources/assets/js/components/Eating/EatingDashboardComponent.vue

            param: {},
            contents: {
               date: "",
            },
            label: ['タンパク質', '脂質', '炭水化物', 'カロリー'],
            datasets: [],
            sub: 0,
        };
    },
    created: function() {
        this.todayDate = this.getDate(this.sub);
    },
    mounted: function() {
        this.graphUpdate();
    },
    methods: {
        getDate: function(sub) {
            var today = new Date();
            return today.getFullYear() + "-" + ('00'+(today.getMonth() + 1)).slice( -2 ) + "-" + ('00'+(today.getDate() + sub)).slice(-2);
        },
        onClickNext: function() {
            this.sub++;
            this.todayDate = this.getDate(this.sub);
            this.graphUpdate();
        },
        onClickPrev: function() {
            this.sub--;
            this.todayDate = this.getDate(this.sub);
            this.graphUpdate();
        },
        onClickInput: function() {
            this.showInputDialogContent = true;
        },
        invokeUpdateList: function() {
            this.graphUpdate();
        },
        graphUpdate: function() {
            var ctx = document.getElementById("eating");
            var self = this;
            this.contents.date = this.todayDate;
            this.param.contents = this.contents;
            this.datasets = [];
            axios.post('api/eating/graph', this.param).then(function(response){
                if(response.data.data != null) {
                    self.datasets.push(response.data.data.protein);
                    self.datasets.push(response.data.data.liqid);
                    self.datasets.push(response.data.data.carbo);
                    self.datasets.push(response.data.data.calorie);
                    var myChart = new Chart(ctx, {
                        type: 'radar',
                        data: {
                            labels: self.label,
                            datasets: [{
                                label: self.todayDate,
                                data: self.datasets,
                                backgroundColor: 'RGBA(225,95,150, 0.5)',
                                borderColor: 'RGBA(225,95,150, 1)',
                                borderWidth: 1,
                                pointBackgroundColor: 'RGB(46,106,177)'
                            }]
                        },
                        options: {
                            title: {
                                display: true,
                                text: '摂取栄養素'
                            },
                            scale:{
                                ticks:{
                                    suggestedMin: 0,
                                    suggestedMax: 100,
                                }
                            }
                        }
                    });
                } else {
                    var myChart = new Chart(ctx, {
                        type: 'radar',
                        data: {
                            labels: self.label,
                            datasets: [
                            ]
                        },
                    });
                }
            }).catch(function(error){
            });
        }

デフォルトはアクセスした当日を表示し、next、prevクリックでそれぞれ次の日、前の日に切り替えることができるようにします。

getDate()で取得する日付の文字列を取得し、これをAPIのパラメータとします。

取得したデータをグラフのdatasets[]に設定すれば、グラフが表示されます。

次回はこのグラフをもっと見やすいようにカスタマイズしていきます。

【ANDROID】【実質北海道一周】UIの実装

さて、

このUIを作成していきます。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/startCity"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="36sp"
        app:layout_constraintBottom_toTopOf="@+id/endCity"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.425"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.528" />

    <TextView
        android:id="@+id/endCity"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="36sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.425"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.591" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="↓"
        android:textSize="36sp"
        app:layout_constraintBottom_toTopOf="@+id/endCity"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.402"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/startCity"
        app:layout_constraintVertical_bias="0.525" />

    <TextView
        android:id="@+id/distanceSection"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="280dp"
        android:textAlignment="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toEndOf="@+id/textView3"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/distanceFromStart"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="364dp"
        android:textAlignment="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toEndOf="@+id/textView3" />
</androidx.constraintlayout.widget.ConstraintLayout>

レイアウトとか、細かいところよくわかっていないので、適当です。

もしかしたら、機種によってレイアウトが崩れるとかもあるかも。

次回は実際に表示する処理を作成していきます。