Repository: trichl/TickTagOpenSource Branch: main Commit: 2730cc4e01e6 Files: 261 Total size: 13.5 MB Directory structure: gitextract_c417dd8h/ ├── .gitattributes ├── LICENSE ├── README.md ├── TickTag3DPrints/ │ └── REV3/ │ ├── TTHousing165mAh_Bottom.stl │ ├── TTHousing165mAh_Lid.stl │ ├── TTHousing30mAhV2_Bottom.stl │ ├── TTHousing30mAhV2_Lid.stl │ ├── TTHousing30mAh_Bottom.stl │ ├── TTHousing30mAh_Lid.stl │ ├── TTHousingLiPoOutside_Bottom.stl │ └── TTHousingLiPoOutside_Lid.stl ├── TickTagAndroidApp/ │ ├── .gitignore │ ├── .idea/ │ │ ├── codeStyles/ │ │ │ └── Project.xml │ │ ├── gradle.xml │ │ ├── misc.xml │ │ ├── runConfigurations.xml │ │ └── vcs.xml │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── timm/ │ │ │ └── ticktagandroidapp/ │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── timm/ │ │ │ │ └── ticktagandroidapp/ │ │ │ │ ├── LogDataStorage.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── RawDataStorage.java │ │ │ │ └── StorageGeneral.java │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable-v24/ │ │ │ │ ├── chart_fade.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout/ │ │ │ │ ├── activity_decoder.xml │ │ │ │ ├── activity_detail.xml │ │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── xml/ │ │ │ └── device_filter.xml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── timm/ │ │ └── ticktagandroidapp/ │ │ └── ExampleUnitTest.java │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── TickTagData/ │ ├── 01_StationaryPosition_1s_Interval/ │ │ ├── 20220221_1s_Interval_10527_fixes.csv │ │ ├── 20220221_1s_Interval_10527_fixes.txt │ │ ├── 20220222_1s_Interval_10842_fixes.csv │ │ ├── 20220222_1s_Interval_10842_fixes.txt │ │ ├── 20220302_1s_Interval_9323_fixes.csv │ │ ├── 20220302_1s_Interval_9323_fixes.txt │ │ ├── 20220309_1s_Interval_10069_fixes.csv │ │ └── 20220309_1s_Interval_10069_fixes.txt │ ├── 02_StationaryPosition_5s_Interval/ │ │ ├── 20220223_5s_Interval_2060_fixes.csv │ │ ├── 20220223_5s_Interval_2060_fixes.txt │ │ ├── 20220223_5s_Interval_2113_fixes.csv │ │ └── 20220223_5s_Interval_2113_fixes.txt │ ├── 03_StationaryPosition_30s_Interval/ │ │ ├── 20220225_30s_Interval_683_fixes.csv │ │ ├── 20220225_30s_Interval_683_fixes.txt │ │ ├── 20220226_30s_Interval_309_fixes.csv │ │ └── 20220226_30s_Interval_309_fixes.txt │ ├── 04_StationaryPosition_60s_Interval/ │ │ ├── 20220227_60s_Interval_474_fixes.csv │ │ ├── 20220227_60s_Interval_474_fixes.txt │ │ ├── 20220228_60s_Interval_372_fixes.csv │ │ └── 20220228_60s_Interval_372_fixes.txt │ ├── 05_StationaryPosition_300s_Interval/ │ │ ├── 20220301_300s_Interval_94_fixes.csv │ │ ├── 20220301_300s_Interval_94_fixes.txt │ │ ├── 20220302_300s_Interval_283_fixes.csv │ │ ├── 20220302_300s_Interval_283_fixes.txt │ │ ├── 20220304_300s_Interval_153_fixes.csv │ │ ├── 20220304_300s_Interval_153_fixes.txt │ │ ├── 20220307_300s_Interval_176_fixes.csv │ │ ├── 20220307_300s_Interval_176_fixes.txt │ │ ├── 20220310_300s_Interval_202_fixes.csv │ │ ├── 20220310_300s_Interval_202_fixes.txt │ │ ├── 20220311_300s_Interval_91_fixes.csv │ │ ├── 20220311_300s_Interval_91_fixes.txt │ │ ├── 20220312_300s_Interval_218_fixes.csv │ │ ├── 20220312_300s_Interval_218_fixes.txt │ │ ├── 20220312_300s_Interval_314_fixes.csv │ │ └── 20220312_300s_Interval_314_fixes.txt │ ├── 06_StationaryPosition_10s_Burst_Every_30s/ │ │ ├── 20220303_10s_Burst_Every_30s_5331_fixes.csv │ │ ├── 20220303_10s_Burst_Every_30s_5331_fixes.txt │ │ ├── 20220304_10s_Burst_Every_30s_5863_fixes.csv │ │ └── 20220304_10s_Burst_Every_30s_5863_fixes.txt │ ├── 07_StationaryPosition_20s_Burst_Every_120s/ │ │ ├── 20220311_20s_Burst_Every_120s_1766_fixes.csv │ │ ├── 20220311_20s_Burst_Every_120s_1766_fixes.txt │ │ ├── 20220311_20s_Burst_Every_120s_4656_fixes.csv │ │ ├── 20220311_20s_Burst_Every_120s_4656_fixes.txt │ │ ├── 20220313_20s_Burst_Every_120s_4280_fixes.csv │ │ └── 20220313_20s_Burst_Every_120s_4280_fixes.txt │ ├── 08_PetDogWalk_1s_Interval/ │ │ ├── 20220303_PetDog_1s_Interval_10309_fixes.csv │ │ ├── 20220303_PetDog_1s_Interval_10309_fixes.txt │ │ ├── 20220304_PetDog_1s_Interval_13105_fixes.csv │ │ ├── 20220304_PetDog_1s_Interval_13105_fixes.txt │ │ ├── 20220307_PetDog_1s_Interval_12445_fixes.csv │ │ └── 20220307_PetDog_1s_Interval_12445_fixes.txt │ ├── 09_PetDogWalk_10s_Burst_Every_15s/ │ │ ├── 20220308_PetDog_10s_Burst_Every_15s_7830_fixes.csv │ │ ├── 20220308_PetDog_10s_Burst_Every_15s_7830_fixes.txt │ │ ├── 20220309_PetDog_10s_Burst_Every_15s_7075_fixes.csv │ │ ├── 20220309_PetDog_10s_Burst_Every_15s_7075_fixes.txt │ │ ├── 20220311_PetDog_10s_Burst_Every_15s_6955_fixes.csv │ │ └── 20220311_PetDog_10s_Burst_Every_15s_6955_fixes.txt │ ├── 10_AntiPoachingHound_1s_Interval/ │ │ ├── 20220209_AntiPoachingHound_1s_Interval_5096_fixes.csv │ │ ├── 20220209_AntiPoachingHound_1s_Interval_5096_fixes.txt │ │ └── 20220209_AntiPoachingHound_1s_Interval_eobs.csv │ └── 11_MyotisMyotis/ │ ├── 20210706_MyotisMyotis_Tag03_29_fixes.csv │ ├── 20210706_MyotisMyotis_Tag04_241_fixes.csv │ ├── 20210706_MyotisMyotis_Tag09_75_fixes.csv │ ├── 20210706_MyotisMyotis_Tag12_410_fixes.csv │ └── 20210706_MyotisMyotis_completeDownload.txt ├── TickTagHardware/ │ ├── GerberProductionFiles/ │ │ ├── CONFIGURATION/ │ │ │ ├── PCBWay_2_layer.cam │ │ │ └── PCBWay_2_layer_with_milling.cam │ │ ├── TickTag-UIB/ │ │ │ └── BOMTickTag-UIB.xlsx │ │ ├── TickTagREV3/ │ │ │ └── BOM-TickTagREV3.xls │ │ └── TickTagREV4/ │ │ └── BOM-TickTagREV4.xlsx │ ├── TickTag-UIB.brd │ ├── TickTag-UIB.sch │ ├── TickTagREV3.brd │ ├── TickTagREV3.sch │ ├── TickTagREV4.brd │ └── TickTagREV4.sch ├── TickTagProgramming/ │ ├── FinalFirmwareBinaries/ │ │ ├── TickTagSoftwareBurst_V201/ │ │ │ ├── EEPROM_CAT24M01.d │ │ │ ├── EEPROM_CAT24M01.o │ │ │ ├── GPS_L70_Light.d │ │ │ ├── GPS_L70_Light.o │ │ │ ├── HelperFunctions.d │ │ │ ├── HelperFunctions.o │ │ │ ├── I2C.d │ │ │ ├── I2C.o │ │ │ ├── Makefile │ │ │ ├── TestFunctions.d │ │ │ ├── TestFunctions.o │ │ │ ├── TickTag.d │ │ │ ├── TickTag.o │ │ │ ├── TickTagSoftwareBurst.eep │ │ │ ├── TickTagSoftwareBurst.elf │ │ │ ├── TickTagSoftwareBurst.hex │ │ │ ├── TickTagSoftwareBurst.lss │ │ │ ├── TickTagSoftwareBurst.srec │ │ │ ├── Time.d │ │ │ ├── Time.o │ │ │ ├── UART.d │ │ │ ├── UART.o │ │ │ ├── main.d │ │ │ ├── main.o │ │ │ └── makedep.mk │ │ ├── TickTagSoftwareBurst_V202_ATTINY1626/ │ │ │ ├── EEPROM_CAT24M01.d │ │ │ ├── EEPROM_CAT24M01.o │ │ │ ├── GPS_L70_Light.d │ │ │ ├── GPS_L70_Light.o │ │ │ ├── HelperFunctions.d │ │ │ ├── HelperFunctions.o │ │ │ ├── I2C.d │ │ │ ├── I2C.o │ │ │ ├── Makefile │ │ │ ├── TestFunctions.d │ │ │ ├── TestFunctions.o │ │ │ ├── TickTag.d │ │ │ ├── TickTag.o │ │ │ ├── TickTagSoftwareBurst.eep │ │ │ ├── TickTagSoftwareBurst.elf │ │ │ ├── TickTagSoftwareBurst.hex │ │ │ ├── TickTagSoftwareBurst.lss │ │ │ ├── TickTagSoftwareBurst.srec │ │ │ ├── Time.d │ │ │ ├── Time.o │ │ │ ├── UART.d │ │ │ ├── UART.o │ │ │ ├── main.d │ │ │ ├── main.o │ │ │ └── makedep.mk │ │ └── TickTagSoftwareBurst_V203_ATTINY1626_BonnetedBats/ │ │ ├── EEPROM_CAT24M01.d │ │ ├── EEPROM_CAT24M01.o │ │ ├── GPS_L70_Light.d │ │ ├── GPS_L70_Light.o │ │ ├── HelperFunctions.d │ │ ├── HelperFunctions.o │ │ ├── I2C.d │ │ ├── I2C.o │ │ ├── Makefile │ │ ├── TestFunctions.d │ │ ├── TestFunctions.o │ │ ├── TickTag.d │ │ ├── TickTag.o │ │ ├── TickTagSoftwareBurst.eep │ │ ├── TickTagSoftwareBurst.elf │ │ ├── TickTagSoftwareBurst.hex │ │ ├── TickTagSoftwareBurst.lss │ │ ├── TickTagSoftwareBurst.srec │ │ ├── Time.d │ │ ├── Time.o │ │ ├── UART.d │ │ ├── UART.o │ │ ├── main.d │ │ ├── main.o │ │ └── makedep.mk │ ├── avrdude/ │ │ ├── ReadmeAVRDUDE.txt │ │ ├── ScriptWriteFusesAndFirmware_V202_ATTINY1626.bat │ │ ├── ScriptWriteFusesAndFirmware_V203_ATTINY1626_BonnetedBats.bat │ │ ├── ScriptWriteFuses_ATTINY1616.bat │ │ ├── ScriptWriteFuses_ATTINY1626.bat │ │ ├── bin/ │ │ │ └── avrdude.conf │ │ └── etc/ │ │ └── avrdude.conf │ └── jtag2updi-master/ │ ├── LICENSE │ ├── README.md │ ├── avrdude.conf │ ├── avrdude.conf.arduino │ ├── build/ │ │ ├── JTAG2UPDI.elf │ │ ├── JTAG2UPDI.hex │ │ └── JTAG2UPDI.lss │ ├── jtag2updi/ │ │ ├── JICE_io.cpp │ │ ├── JICE_io.h │ │ ├── JTAG2.cpp │ │ ├── JTAG2.h │ │ ├── NVM.h │ │ ├── NVM_v2.h │ │ ├── UPDI_hi_lvl.cpp │ │ ├── UPDI_hi_lvl.h │ │ ├── UPDI_lo_lvl.cpp │ │ ├── UPDI_lo_lvl.h │ │ ├── crc16.cpp │ │ ├── crc16.h │ │ ├── dbg.cpp │ │ ├── dbg.h │ │ ├── jtag2updi.cpp │ │ ├── jtag2updi.ino │ │ ├── parts.h │ │ ├── sys.cpp │ │ ├── sys.h │ │ ├── updi_io.cpp │ │ ├── updi_io.h │ │ └── updi_io_soft.cpp │ ├── make.bat │ ├── make.sh │ └── tools/ │ └── avrjtagicev2/ │ └── README.md ├── TickTagSerialDownload/ │ ├── README.md │ ├── TickTag_Serial_Download.py │ └── requirements.txt └── TickTagSoftwareBurst/ ├── .vs/ │ └── TickTagSoftwareBurst/ │ └── v14/ │ └── .atsuo ├── TickTagSoftwareBurst/ │ ├── Configuration.h │ ├── EEPROM_CAT24M01.cpp │ ├── EEPROM_CAT24M01.h │ ├── GPS_L70_Light.cpp │ ├── GPS_L70_Light.h │ ├── HelperFunctions.cpp │ ├── HelperFunctions.h │ ├── I2C.cpp │ ├── I2C.h │ ├── TestFunctions.cpp │ ├── TestFunctions.h │ ├── TickTag.cpp │ ├── TickTag.h │ ├── TickTagSoftwareBurst.componentinfo.xml │ ├── TickTagSoftwareBurst.cppproj │ ├── Time.cpp │ ├── Time.h │ ├── UART.cpp │ ├── UART.h │ └── main.cpp └── TickTagSoftwareBurst.atsln ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 trichl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # I Repository Introduction This repository contains the entire production-ready design (hardware, software, user interface, 3D-printable housing, assembly instructions) of the open-source TickTag GPS logger. # II Hardware Production * See sub folder [TickTagHardware](TickTagHardware) * Schematics (.sch) and boards (.brd) are designed in Autodesk Eagle 9.5.2 * PCBs were produced and assembled by [PCBWay](https://www.pcbway.com) (production-ready Gerber files and PCBWay settings in [TickTagHardware/GerberProductionFiles](TickTagHardware/GerberProductionFiles)) * Production settings: * **REV3**: 24.9 x 10.5 mm, 2 layers, 0.15 mm thickness (flex), 0.35 mm min hole size, immersion gold (ENIG) surface finish (1U"), 0.06 mm min track spacing, 1 oz Cu finished copper, polyimide flex material ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev3.jpg?raw=true) * **REV4**: 23.61 x 10.06 mm, 2 layers, 0.2 mm thickness, 0.25 mm min hole size, immersion gold (ENIG) surface finish (1U"), tenting vias, 5/5 mil min track spacing, 1 oz Cu finished copper, FR-4 TG150 material ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev4top.png?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev4bottom.png?raw=true) * **User interface board**: 66.4 x 17.8 mm, 2 layers, 1 mm thickness, 0.3 mm min hole size, immersion gold (ENIG) surface finish (1U"), tenting vias, 6/6 mil min track spacing, 1 oz Cu finished copper, FR-4 TG130 material * Differences between REV3 and REV4: * 0.2 mm PCB instead of 0.15 mm (more robust) * PCB slots instead of pads for soldering LiPos directly to the PCB * New load switch (SiP32431DNP3-T1GE4), as the old load switch has long lead times * Reduced the footprint sizes of some of the capacitors and resistors from 0402 to 0201 # III Programming and Assembly ## IDE for Software Development (Windows) * Atmel Studio 7.0: https://www.microchip.com/en-us/tools-resources/develop/microchip-studio * Programming language: C / C++ ## Android App * Developed in Android Studio 3.5.1: https://developer.android.com/studio ## Using an Arduino Nano for Flashing * Follow the instructions on https://github.com/ElTangas/jtag2updi * The 4.7k resistor is already integrated on the user interface board and does not need to be added * Alternatively software can be flashed with the official Atmel-ICE: https://www.microchip.com/en-us/development-tool/ATATMEL-ICE ## Flashing the Firmware Flashing should be done before soldering a battery to the tag. ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/uibsettings3.png?raw=true) 1. Check if the yellow jumper (E) connects "1" and "2" ("ldo") 2. Gently click the tag on the user interface board (without battery attached to it) (A), mind the correct orientation of the tag 3. Connect D6 of the Arduino Nano to the UPDI pin of the user interface board (or use an Atmel-ICE instead of an Arduino Nano) 4. Connect the user interface board to a computer (red LED turns on) (G), connect the Arduino Nano to the same computer 5. Slide the UPDI button on the user interface board (H) to ON 6. Go to [TickTagProgramming/avrdude](TickTagProgramming/avrdude), open ScriptWriteFuses_ATTINY1626.bat (or ScriptWriteFuses_ATTINY1616.bat, if you use the 1616) with a text editor and enter the COM port of the Arduino Nano on your computer 7. Execute ScriptWriteFuses_ATTINY1626.bat to write the configuration fuses of the ATTINY. ( It may be possible that executing the script raises an error ie. missing dll file. In this case, try to download and install WinAVR [here](https://sourceforge.net/projects/winavr/) ) 8. Open Atmel Studio 7.0 and load the project [TickTagSoftwareBurst](TickTagSoftwareBurst) (you might need to setup the project first, and set the current device to ATtiny1626) 9. 1. Configure the ATTINY programming via Arduino Nano under Tools -> External Tools 2. To choose the command, select the three dots and find the location of the TickTagOpenSource repository. Then navigate to TickTagProgramming\avrdude\bin and select avrdude.exe 3. For the Arguments: "-P COM7 -c jtag2updi -p t1626 -U flash:w:$(ProjectDir)Debug\\$(TargetName).hex:i", COM7 needs to be replaced with the COM port of the Arduino Nano: ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/atmelconfig.png?raw=true) 10. Press F7 to compile the firmware 11. Press Tools -> jtag2updi ATtiny1626 to flash the firmware ## Tag Assembly ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev3soldered.jpg?raw=true) * If the firmware is successfully flashed onto the microcontroller you can solder a battery to the TickTag battery terminals (plus and minus is written on the tag) * No battery protection circuit is needed, the battery is protected by software * You can use a glass fiber pen to roughen the LiPo tabs, which makes soldering of the aluminium tabs more easy (e.g., with a Laeufer 69119 pen) * Keep the soldering work on the LiPo very short, otherwise the battery might be damaged due to high temperatures * Keep the area below the TickTag as flat as possible, otherwise you might not be able to click the tag on the user interface board anymore # IV Tag Manual ## Charging the Battery **WARNING**: never connect the TickTag to the user interface board when the 3-pin jumper is set to "ldo", otherwise the LiPo will be damaged permanently. The battery can be recharged directly on the breakout board: ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/uib2.jpg?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/uibsettings1.png?raw=true) 1. Check if the yellow jumper connects (E) "3" and "2" ("lipo") 2. Gently click the tag on the user interface board (with battery attached to it) (A), mind the correct orientation of the tag 3. Connect the USB connector to a computer or powerbank (red LED turns on) (G) 4. Slide the charge button on the user interface board (F) to the left (ON) 5. A green LED on the user interface board turns on and indicates that the battery is being charged 6. In case the tag was previously activated, it first needs to be deactivated: * Wait some minutes until battery is charged a bit * Press the white button (D) for 5 seconds to restart the tag and enter menue * Green LED on the tag will blink 5 times to indicate download mode and system restart 7. Wait until the green LED on the user interface board turns off (battery charged) * This can take hours (default charge current: 15 mA) * For example: an empty 30 mAh lipo battery needs 2 hours to be fully charged * For example an empty 120 mAh lipo battery needs 8 hours to be fully charged 8. The tag can be activated again (see chapter "Activation") * Warning: If the memory is full the tag can not be reactivated (data download necessary, see chapter "Data Download, Configuration and Memory Reset") ## Activation * Prerequisites * The tags need to be connected to a charged lithium polymer battery (plus and minus pads on the tag are soldered to the battery) * If the battery voltage is too low, the tag won’t start (please charge the battery), see chapter "Charge Battery" * If the data memory is full, the tag won't start (please download the data and reset the memory), see chapter "Data Download, Configuration and Memory Reset" * Check the location of the green LED on the tag (it will give you visual feedback): ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev3led.jpg?raw=true) ### Activation option 1: by a wire ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev3activation1.jpg?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/activationwire.png?raw=true) 1. Gently touch with one end of a conducting wire the ground connection where the battery minus is soldered to 2. Gently touch with the other end of the wire the hole marked with "A" (or "ST") 3. Once the tag starts blinking green, remove the wire immediately (should not take longer than 2 seconds) 4. The tag blinks 7 times to indicate that it’s activated, then waits for 700 ms and is blinking again to indicate battery status (1 time = battery low, 7 times = battery is full) 5. The green LED is located on the back side of the tag, so it's best to hold the breakout board sideways to see the blinking LED under the tag 6. If it doesn't blink at all: battery voltage might be low or the tag is already activated 7. If the tag blinks 5 times it entered download mode (was already activated), please wait a minute and start again from the beginning 8. The tag is activated and will start sampling GPS data after 10 seconds (default configuration, can be changed, see chapter "Data Download, Configuration and Memory Reset") ### Activation option 2: on breakout board ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev3click.jpg?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/uibsettings1.png?raw=true) 1. Locate the click connector on the tag (see picture above) 2. Take a look at the breakout board, do not connect the USB power connector (G) to the computer or phone (no external power needed for activation) 3. Gently click the tag on the user interface board (with battery attached to it) (A), mind the correct orientation of the tag * Take care of the correct orientation of the tag as shown in the photo, otherwise a short circuit might permanently damage the tag 4. Now press the white button (D) for two seconds until the tag starts to blink * Do not press the button longer than some seconds * The tag blinks 7 times to indicate that it’s activated, then waits for 700 ms and is blinking again to indicate battery status (1 time = battery low, 7 times = battery is full) * The green LED is located on the back side of the tag, so it's best to hold the breakout board sideways to see the blinking LED under the tag * If it doesn't blink at all: battery voltage might be low or the tag is already activated * If the tag blinks 5 times it entered download mode (was already activated), please wait a minute and start again from the beginning 5. The tag is activated and will start sampling GPS data after 10 seconds (default configuration, can be changed, see chapter "Data Download, Configuration and Memory Reset") ## After the Activation (Data Sampling) * After the activation delay, the tag starts blinking green every second and tries to get the current time via GPS satellites. If it can’t obtain the time within 120 seconds, it will sleep for 15 minutes and will try again. * **IMPORTANT**: If blinking configuration is set to false the tag will completely stop blinking at all after getting the current UTC time * If you have configured the tag to only sample GPS at certain times: * **IMPORTANT**: Before attaching the tag to the animal (e.g., the day before): go outside, activate the tag, wait until it starts blinking, wait until the green LED stays on for 2 seconds (tag got the time and went to sleep mode). * The tag samples GPS data (continuously or from time to time, depending on the configuration) * The module deactivates itself if the battery voltage is low (default configuration: 3.3V) or if the memory is full (maximum 13,100 fixes) ## On-Animal Mounting * **IMPORTANT**: The small antenna of the TickTag is disturbed by any electrically conducting material nearby, so keep the antenna away from other tags, VHF/UHF tags, batteries, screws or similar materials (at least 5 cm). * **IMPORTANT**: The antenna can easily break if bent too strong. Try to protect the antenna with a 3D-printed housing (for example with ASA material). * **IMPORTANT**: The battery needs to be located behind the tag (see picture below), never under the tag, as it will disturb the antenna. * **IMPORTANT**: Mount the tag on the flat side (connector facing down, top side with antenna facing the sky). The photo shows the top side that should face the sky: ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/antenna.png?raw=true) * **IMPORTANT**: Do not glue anything on the connector that can't be removed, otherwise data cannot be downloaded (Bostik Blu Tack is a glue that can be easily removed afterwards): ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/rev3click.jpg?raw=true) ## Data Download, Configuration and Memory Reset ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/uibsettings1.png?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/uibphone.jpg?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/termite.png?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/mobaxterm.png?raw=true) ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/app.png?raw=true) 1. Choose a serial software * **Option 1**: Download a serial program for your computer * For example (Windows 10): Termite: https://www.compuphase.com/software_termite.htm (see screenshot above for configuration) * For example (Windows 10): MobaXTerm: https://mobaxterm.mobatek.net/ (see screenshot above for configuration) * The Arduino Serial Monitor also works fine * **Option 2**: Use the TickTag Android app [TickTagAndroidApp](TickTagAndroidApp) and an USB OTG adapter to connect the breakout board with the phone 2. The battery needs to be connected (soldered) to the tag 3. Check if the yellow jumper connects (E) "3" and "2" ("lipo") 4. Gently click the tag on the user interface board (with battery attached to it) (A), mind the correct orientation of the tag 5. Connect the USB connector to a computer or Android phone (red LED turns on) (G) 6. Slide the charge button on the user interface board (F) to the left (ON) to charge the battery while downloading data (not mandatory, but recommended) 7. **For option 1**: Open the serial program on your computer with following settings: * COM port: select the CP2104N COM port from the list * Baud rate: 9600 * Default serial settings (8 data bits, 1 stop bit, no parity) 8. Click on CONNECT in your serial program or the Android app 9. Press the download button (D) for 5 seconds (not longer) until the green LED on the tag blinks for 5 times 10. Data download is shown in your serial program (in CSV format) 11. After the download the tag shows a little menu for configuration * Data download might take some minutes (if memory is full) * Data download can be interrupted by pressing the download button once (short), data will not be deleted 12. In the command line of your serial program enter "1" and press return to reset the memory (when using the Android app: press the "Reset memory" button) 13. The tag will confirm that the memory is now empty 14. The tag will restart into the activation state after some time (waiting on tag activation, see chapter "Activation") Example data output of the serial program (or Android app): ``` ---TICK-TAG--- *START MEMORY* UVs: 0, TOs: 0/0, ErrorsOrGF: 0, TTFF: 68 Fixes: 10, Avg. TTF: 11 s, Avg. HDOP (x10): 16 count,timestamp,lat,lon 1,1635235981,47.74340,8.99910 2,1635236130,47.74346,8.99922 3,1635236352,47.74340,8.99889 4,1635236506,47.74327,8.99894 5,1635236682,47.74336,8.99879 6,1635238092,47.74340,8.99824 7,1635238313,47.74304,8.99896 8,1635238411,47.74329,8.99883 9,1635238511,47.74332,8.99878 10,1635238610,47.74325,8.99881 *END MEMORY* V201, ID: 30-2509, 3533mV, 01b SETTINGS: - Min voltage: 3300 mV - Frequency: 90 s - Min HDOP (x10): 30 - Activation delay: 60 s - Geofencing: 0 - Burst duration: 0 s - Time: 8:00 - 14:00 UTC 0 Read memory 1 Reset memory 2 Set min. voltage 3 Set frequency 4 Set accuracy 5 Set activation delay 6 Set times 7 Toggle geofencing ON/OFF 8 Set burst duration 9 Exit ``` ## Configuration Parameters * **Read memory**: printing all stored GPS fixes as CSV-compatible list (count, timestamp in UTC, latitude, longitude) * **Reset memory**: deleting all stored GPS fixes * **Min. voltage (3000 - 4250, in mV)**: recording is stopped when battery voltage (open-circuit, no load) drops below that threshold * **Frequency (1 - 16382, in s)**: GPS fix attempt frequency, between 1 and 5 s the TickTag keeps the GPS module constantly powered and puts the device in fitness low power mode * **Accuracy (1 - 250, HDOP x 10)**: HDOP value (times 10, 30 = 3.0) that tries to be achieved within 9 seconds after getting the first positional estimate * **Activation delay (10 - 16382, in s)**: delay after activation before the tag starts recording data * **Sampling times (0000 - 2359, time within day)**: daily recording time window * **Geo-fencing (true/false)**: if set to true the first GPS fix becomes the home location and only fixes outside a 300 m radius are stored (10 min hibernation afterwards) * **Blinking (true/false)**: if set to true the tag blinks every second when GPS is active ## Default Configuration * After flashing the firmware the default configuration is set to: * **Min. voltage (3000 - 4250, in mV)**: 3000 * **Frequency (1 - 16382, in s)**: 30 * **Accuracy (1 - 250, HDOP x 10)**: 30 * **Activation delay (10 - 16382, in s)**: 10 * **Sampling times (0000 - 2359, time within day)**: always on * **Geo-fencing (true/false)**: false * **Blinking (true/false)**: false ## State Machine ![Image](https://github.com/trichl/TickTagOpenSource/blob/main/TickTagImages/statemachine.png?raw=true) # V Data Compression Algorithm GPS data is stored on the 128 kByte EEPROM with a lossless compression algorithm. GPS positions are stored with 5 decimal places (accuracy: 1.11 m). * **Latitute (A)**: stored in 25 Bit (unsigned): compressedLatitude = (latitude * 100000) + 9000000 * **Longitude (B)**: stored in 26 Bit (unsigned): compressedLongitude = (longitude * 100000) + 18000000 * **Timestamp (C)**: stored in 29 Bit (unsigned): compressedTimestamp = timestamp - 1618428394 * Storage pattern of Bits: **AAAAAAAA AAAAAAAA AAAAAAAA ABBBBBBB BBBBBBBB BBBBBBBB BBBCCCCC CCCCCCCC CCCCCCCC CCCCCCCC** * Total length: 10 Byte per GPS fix # VI 3D-printed Housings * Check [TickTag3DPrints](TickTag3DPrints) for .STL files (designed in Fusion 360) * Housings were printed with ASA filament on a Prusa Mini (0.4 mm nozzle) * PrusaSlider 2.2.0 settings: 0.45 mm extrusion width, 0.10 mm DETAIL settings # VII Data * Check [TickTagData](TickTagData) for data results ================================================ FILE: TickTagAndroidApp/.gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx ================================================ FILE: TickTagAndroidApp/.idea/codeStyles/Project.xml ================================================
xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
.*:name http://schemas.android.com/apk/res/android
name ^$
style ^$
.* ^$ BY_NAME
.* http://schemas.android.com/apk/res/android ANDROID_ATTRIBUTE_ORDER
.* .* BY_NAME
================================================ FILE: TickTagAndroidApp/.idea/gradle.xml ================================================ ================================================ FILE: TickTagAndroidApp/.idea/misc.xml ================================================ ================================================ FILE: TickTagAndroidApp/.idea/runConfigurations.xml ================================================ ================================================ FILE: TickTagAndroidApp/.idea/vcs.xml ================================================ ================================================ FILE: TickTagAndroidApp/app/.gitignore ================================================ /build ================================================ FILE: TickTagAndroidApp/app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 29 buildToolsVersion "29.0.2" defaultConfig { applicationId "com.timm.ticktagandroidapp" minSdkVersion 25 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } repositories { maven { url 'https://jitpack.io' } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' implementation 'org.nanohttpd:nanohttpd:2.3.1' implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' implementation 'com.github.mik3y:usb-serial-for-android:3.4.2' } ================================================ FILE: TickTagAndroidApp/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: TickTagAndroidApp/app/src/androidTest/java/com/timm/ticktagandroidapp/ExampleInstrumentedTest.java ================================================ package com.timm.ticktagandroidapp; import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumented test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.timm.ticktagandroidapp", appContext.getPackageName()); } } ================================================ FILE: TickTagAndroidApp/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: TickTagAndroidApp/app/src/main/java/com/timm/ticktagandroidapp/LogDataStorage.java ================================================ package com.timm.ticktagandroidapp; import android.content.Context; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Locale; public class LogDataStorage { private static final String LOGFILENAME = "tickTagLog.txt"; public static String logText(Context context, String text) { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()); String currentDateandTime = sdf.format(new Date()); String fullFileName = StorageGeneral.getStorageDirectory(context) + "/" + currentDateandTime + "_" + LOGFILENAME; File fileToLogTo = new File(fullFileName); if(fileToLogTo.exists()) { return ""; } try { fileToLogTo.createNewFile(); } catch (IOException e) { return ""; } try { FileOutputStream stream = new FileOutputStream(fullFileName, true); stream.write(text.getBytes(Charset.forName("UTF-8"))); stream.close(); } catch (IOException e1) { return ""; } return fullFileName; } } ================================================ FILE: TickTagAndroidApp/app/src/main/java/com/timm/ticktagandroidapp/MainActivity.java ================================================ package com.timm.ticktagandroidapp; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.AppCompatButton; import android.Manifest; import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.InsetDrawable; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.InputType; import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import com.hoho.android.usbserial.driver.UsbSerialDriver; import com.hoho.android.usbserial.driver.UsbSerialPort; import com.hoho.android.usbserial.driver.UsbSerialProber; import com.hoho.android.usbserial.util.SerialInputOutputManager; import java.io.IOException; import java.util.List; public class MainActivity extends AppCompatActivity implements SerialInputOutputManager.Listener { private String INTENT_ACTION_GRANT_USB = BuildConfig.APPLICATION_ID + ".GRANT_USB"; SerialInputOutputManager usbIoManager; Button bStartStop, bStoreOutput, bOpenDownloads, bClearOutput; Button b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, bEnter, bReadMemory, bReset, bSetVoltage, bSetFrequency, bSetAccuracy, bSetDelay, bSetHour, bToggleGeofencing, bToggleBlinking, bSetBurstDuration, bExit; TextView tOutput, tHeadline; RelativeLayout rLoadingPanel; LinearLayout lAlertPressButton, lAlertNotConnected, lButtons, lButtons2; ScrollView scScroll; UsbSerialPort usbPort = null; Thread thread; boolean tickTagConnected = false; private void checkPermissions() { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if(this.checkSelfPermission(Manifest.permission.WRITE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { if(!this.shouldShowRequestPermissionRationale(Manifest.permission.WRITE_SETTINGS)) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("This app needs background location access"); builder.setMessage("Please grant location access so this app can detect beacons in the background."); builder.setPositiveButton(android.R.string.ok, null); builder.setOnDismissListener(new DialogInterface.OnDismissListener() { @TargetApi(23) @Override public void onDismiss(DialogInterface dialog) { requestPermissions(new String[]{Manifest.permission.WRITE_SETTINGS}, 0); } }); builder.show(); } else { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Functionality limited"); builder.setMessage("Since background location access has not been granted, this app will not be able to discover beacons in the background. Please go to Settings -> Applications -> Permissions and grant background location access to this app."); builder.setPositiveButton(android.R.string.ok, null); builder.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) {} }); builder.show(); } } } } public void showMessage(String headline, String text) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(headline); builder.setMessage(text); builder.setPositiveButton(android.R.string.ok, null); builder.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) {} }); builder.show(); } void setButtonConnected() { bStartStop.setText("DISCONNECT"); if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP && bStartStop instanceof AppCompatButton) { // don't change color } else { bStartStop.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#B9F6CA"))); } lAlertPressButton.setVisibility(View.VISIBLE); lAlertNotConnected.setVisibility(View.GONE); //lButtons.setVisibility(View.VISIBLE); lButtons2.setVisibility((View.VISIBLE)); } void setButtonDisconnected() { bStartStop.setText("CONNECT"); if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP && bStartStop instanceof AppCompatButton) { // don't change color } else { bStartStop.setBackgroundTintList(ColorStateList.valueOf(Color.parseColor("#FF80AB"))); } lAlertPressButton.setVisibility(View.GONE); lAlertNotConnected.setVisibility(View.VISIBLE); //lButtons.setVisibility(View.GONE); lButtons2.setVisibility((View.GONE)); } @Override protected void onPause() { //Toast.makeText(this, "Gateway is still running in background!", Toast.LENGTH_LONG).show(); super.onPause(); } @Override public void onBackPressed() { super.onBackPressed(); } @Override protected void onResume() { super.onResume(); } public void updateHeadlineText() { if(tHeadline != null) tHeadline.setText("TickTag Android App\nFree memory: " + StorageGeneral.getAvailableInternalMemorySizeAsStringInMByte() + " MByte"); } private void setAlertWindows() { lAlertPressButton.setVisibility(View.GONE); lAlertNotConnected.setVisibility(View.GONE); } @Override public void onNewData(final byte[] data) { //System.out.println(new String(data)); runOnUiThread(new Runnable() { @Override public void run() { tOutput.append(new String(data)); lAlertPressButton.setVisibility(View.GONE); scScroll.post(new Runnable() { public void run() { scScroll.fullScroll(ScrollView.FOCUS_DOWN); } }); } }); } @Override public void onRunError(Exception e) { runOnUiThread(new Runnable() { @Override public void run() { showMessage("Disconnected", "USB disconnected!"); tickTagConnected = false; setButtonDisconnected(); } }); } void checkIfUSBConnectable() { setButtonDisconnected(); // Find all available drivers from attached devices. UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); if(availableDrivers.isEmpty()) { showMessage("UIB not connected", "Could not detect a user interface board at the USB port of the phone! Please attach the TickTag to the user interface board and then connect it via USB OTG cable to your phone."); return; } // Open a connection to the first available driver. UsbSerialDriver driver = availableDrivers.get(0); UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); if(connection == null) { PendingIntent usbPermissionIntent = PendingIntent.getBroadcast(MainActivity.this, 0, new Intent(INTENT_ACTION_GRANT_USB), 0); manager.requestPermission(driver.getDevice(), usbPermissionIntent); } usbPort = driver.getPorts().get(0); // Most devices have just one port (port 0) try { usbPort.open(connection); } catch (IOException e) { e.printStackTrace(); showMessage("Error", "Could not open the serial port!"); return; } try { usbPort.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); } catch (IOException e) { e.printStackTrace(); showMessage("Error", "Could not set serial parameters!"); return; } usbIoManager = new SerialInputOutputManager(usbPort, MainActivity.this); usbIoManager.start(); setButtonConnected(); tickTagConnected = true; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.getSupportActionBar().hide(); setContentView(R.layout.activity_main); bStartStop = findViewById(R.id.bstartstop); bStoreOutput = findViewById(R.id.bstoreoutput); bClearOutput = findViewById(R.id.bclearoutput); bOpenDownloads = findViewById(R.id.bopendownloads); tOutput = findViewById(R.id.toutput); rLoadingPanel = findViewById(R.id.rloadingpanel); tHeadline = findViewById(R.id.theadline); scScroll = findViewById(R.id.scscroll); lAlertPressButton = findViewById(R.id.lalertpressbutton); lAlertNotConnected = findViewById(R.id.lalertnotconnected); lButtons = findViewById(R.id.lbuttons); lButtons2 = findViewById(R.id.lbuttons2); b0 = findViewById(R.id.b0); b1 = findViewById(R.id.b1); b2 = findViewById(R.id.b2); b3 = findViewById(R.id.b3); b4 = findViewById(R.id.b4); b5 = findViewById(R.id.b5); b6 = findViewById(R.id.b6); b7 = findViewById(R.id.b7); b8 = findViewById(R.id.b8); b9 = findViewById(R.id.b9); bEnter = findViewById(R.id.benter); bReadMemory = findViewById(R.id.bread); bReset = findViewById(R.id.breset); bSetVoltage = findViewById(R.id.bsetvoltage); bSetFrequency = findViewById(R.id.bsetfrequency); bSetAccuracy = findViewById(R.id.bsetaccuracy); bSetDelay = findViewById(R.id.bsetdelay); bSetHour = findViewById(R.id.bsethour); bToggleGeofencing = findViewById(R.id.btogglegeofencing); bToggleBlinking = findViewById(R.id.btoggleblinking); bSetBurstDuration = findViewById(R.id.bsetburstduration); bExit = findViewById(R.id.bexit); rLoadingPanel.setVisibility(View.GONE); setAlertWindows(); updateHeadlineText(); checkIfUSBConnectable(); /*thread = new Thread() { @Override public void run() { try { while(!thread.isInterrupted()) { Thread.sleep(1000); runOnUiThread(new Runnable() { @Override public void run() { scScroll.post(new Runnable() { public void run() { scScroll.fullScroll(ScrollView.FOCUS_DOWN); } }); } }); } } catch (InterruptedException e) {} } }; thread.start();*/ bStartStop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(tickTagConnected) { if(usbPort != null) { try { usbPort.close(); // triggers onRunError } catch (IOException e) { e.printStackTrace(); showMessage("Error", "Could not close USB port!"); } } } else { checkIfUSBConnectable(); } } }); bClearOutput.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { tOutput.setText(""); //tOutput.append("Hallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\nHallo\n"); Toast.makeText(MainActivity.this, "Output cleared!", Toast.LENGTH_LONG).show(); } }); bStoreOutput.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(tOutput.getText().toString().isEmpty()) { Toast.makeText(MainActivity.this, "Output is empty, nothing to store!", Toast.LENGTH_LONG).show(); } else { String outputString = tOutput.getText().toString(); outputString = outputString.replace("\r", ""); String storedTo = LogDataStorage.logText(MainActivity.this, outputString); if (storedTo.length() > 0) { showMessage("Saved", "Output stored to: " + storedTo); } else { showMessage("Error", "Could not store output!"); } } } }); bOpenDownloads.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Uri selectedUri = Uri.parse(MainActivity.this.getExternalFilesDir(null).getPath()); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(selectedUri, "resource/folder"); if(intent.resolveActivityInfo(getPackageManager(), 0) != null) { startActivity(intent); } else { Toast.makeText(MainActivity.this, "No app found to view folders, please download a file manager", Toast.LENGTH_LONG).show(); } } }); b0.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "0"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "1"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "2"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b3.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "3"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b4.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "4"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b5.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "5"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b6.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "6"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b7.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "7"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b8.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "8"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); b9.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "9"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); bEnter.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } } }); } }); bReadMemory.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "0\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); Toast.makeText(MainActivity.this, "Download might take a while! Press 'Store Output' afterwards to store downloaded data as text file.", Toast.LENGTH_LONG).show(); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); bReset.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Do you really want to reset the memory of the TickTag? All GPS data will be deleted."); alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "1\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bSetVoltage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Enter mV"); final EditText input = new EditText(MainActivity.this); input.setInputType(InputType.TYPE_CLASS_NUMBER); input.setRawInputType(Configuration.KEYBOARD_12KEY); input.setHint("3000 - 4250"); alert.setView(input); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { final String sendText = input.getText().toString(); runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { if(sendText.length() > 0) { String input = "2\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } final String input2 = sendText + "\n\r"; new Handler().postDelayed(new Runnable() { @Override public void run() { tOutput.append(input2); try { usbPort.write(input2.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } }, 500); } else Toast.makeText(MainActivity.this, "Invalid input!", Toast.LENGTH_LONG).show(); } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bSetFrequency.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Enter GPS frequency in seconds (1 - 5 = stay-on)"); final EditText input = new EditText(MainActivity.this); input.setInputType(InputType.TYPE_CLASS_NUMBER); input.setRawInputType(Configuration.KEYBOARD_12KEY); input.setHint("1 - 16382"); alert.setView(input); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { final String sendText = input.getText().toString(); runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { if(sendText.length() > 0) { String input = "3\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } final String input2 = sendText + "\n\r"; new Handler().postDelayed(new Runnable() { @Override public void run() { tOutput.append(input2); try { usbPort.write(input2.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } }, 500); } else Toast.makeText(MainActivity.this, "Invalid input!", Toast.LENGTH_LONG).show(); } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bSetAccuracy.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Enter accuracy (HDOP x 10, 30 = 3.0)"); final EditText input = new EditText(MainActivity.this); input.setInputType(InputType.TYPE_CLASS_NUMBER); input.setRawInputType(Configuration.KEYBOARD_12KEY); input.setHint("10 - 250"); alert.setView(input); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { final String sendText = input.getText().toString(); runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { if(sendText.length() > 0) { String input = "4\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } final String input2 = sendText + "\n\r"; new Handler().postDelayed(new Runnable() { @Override public void run() { tOutput.append(input2); try { usbPort.write(input2.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } }, 500); } else Toast.makeText(MainActivity.this, "Invalid input!", Toast.LENGTH_LONG).show(); } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bSetDelay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Enter activation delay in seconds"); final EditText input = new EditText(MainActivity.this); input.setInputType(InputType.TYPE_CLASS_NUMBER); input.setRawInputType(Configuration.KEYBOARD_12KEY); input.setHint("10 - 16382"); alert.setView(input); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { final String sendText = input.getText().toString(); runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { if(sendText.length() > 0) { String input = "5\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } final String input2 = sendText + "\n\r"; new Handler().postDelayed(new Runnable() { @Override public void run() { tOutput.append(input2); try { usbPort.write(input2.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } }, 500); } else Toast.makeText(MainActivity.this, "Invalid input!", Toast.LENGTH_LONG).show(); } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bSetHour.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Set ON/OFF hour"); final EditText inputOn = new EditText(MainActivity.this); inputOn.setInputType(InputType.TYPE_CLASS_NUMBER); inputOn.setRawInputType(Configuration.KEYBOARD_12KEY); inputOn.setHint("UTC time to turn ON (format: HHMM)"); final EditText inputOff = new EditText(MainActivity.this); inputOff.setInputType(InputType.TYPE_CLASS_NUMBER); inputOff.setRawInputType(Configuration.KEYBOARD_12KEY); inputOff.setHint("UTC time to turn OFF (format: HHMM)"); LinearLayout lay = new LinearLayout(MainActivity.this); lay.setOrientation(LinearLayout.VERTICAL); lay.addView(inputOn); lay.addView(inputOff); alert.setView(lay); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { final String onText = inputOn.getText().toString(); final String offText = inputOff.getText().toString(); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "TEST: " + onText, Toast.LENGTH_LONG).show(); if(tickTagConnected) { if((onText.length() > 0) && (offText.length() > 0)) { final String input1 = "6\n\r"; final String input2 = onText + "\n\r"; final String input3 = offText + "\n\r"; // input 1 tOutput.append(input1); try { usbPort.write(input1.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } // input 2 new Handler().postDelayed(new Runnable() { @Override public void run() { tOutput.append(input2); try { usbPort.write(input2.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } // input 3 new Handler().postDelayed(new Runnable() { @Override public void run() { tOutput.append(input3); try { usbPort.write(input3.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } } }, 500); } }, 500); } else Toast.makeText(MainActivity.this, "Invalid input!", Toast.LENGTH_LONG).show(); } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bToggleGeofencing.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Do you want to toggle geofencing mode ON/OFF?"); alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "7\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bSetBurstDuration.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Enter burst duration in seconds"); final EditText input = new EditText(MainActivity.this); input.setInputType(InputType.TYPE_CLASS_NUMBER); input.setRawInputType(Configuration.KEYBOARD_12KEY); input.setHint("0 - 250"); alert.setView(input); alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { final String sendText = input.getText().toString(); runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { if(sendText.length() > 0) { String input = "8\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); return; } final String input2 = sendText + "\n\r"; new Handler().postDelayed(new Runnable() { @Override public void run() { tOutput.append(input2); try { usbPort.write(input2.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } }, 500); } else Toast.makeText(MainActivity.this, "Invalid input!", Toast.LENGTH_LONG).show(); } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bToggleBlinking.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Do you want to toggle blinking mode ON/OFF?"); alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "b\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); bExit.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this); alert.setTitle("Do you want to exit the menu (TickTag goes to sleep until activation)?"); alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { runOnUiThread(new Runnable() { @Override public void run() { if(tickTagConnected) { String input = "9\n\r"; tOutput.append(input); try { usbPort.write(input.getBytes(), 100); } catch (IOException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Failed to send command to TickTag!", Toast.LENGTH_LONG).show(); } } else Toast.makeText(MainActivity.this, "Not connected to TickTag!", Toast.LENGTH_LONG).show(); } }); } }); alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }); alert.show(); } }); } } ================================================ FILE: TickTagAndroidApp/app/src/main/java/com/timm/ticktagandroidapp/RawDataStorage.java ================================================ package com.timm.ticktagandroidapp; import android.content.Context; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; class RawDataStorage { private static final String FILENAME_BASE = "ESP32Data.bin"; private File fileToStoreTo; private String fullFileName = ""; private String fileNameWithoutDirectory = ""; private int selectedItem; public String getStoragePath() { return fullFileName; } public String getFileNameWithoutDirectory() { return fileNameWithoutDirectory; } public void testWrite() { byte[] fileContent = new byte[20]; for(int i=0; i<20; i++) { fileContent[i] = (byte) i; } writeToFile(fileContent); } public boolean writeToFile(byte[] data) { if(fileToStoreTo == null) return false; if(!fileToStoreTo.exists()) { try { fileToStoreTo.createNewFile(); } catch (IOException e) { return false; } } try { FileOutputStream stream = new FileOutputStream(fullFileName, true); stream.write(data); stream.close(); } catch (IOException e1) { return false; } return true; } public RawDataStorage(Context context) { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()); String currentDateandTime = sdf.format(new Date()); fullFileName = StorageGeneral.getStorageDirectory(context) + "/" + currentDateandTime + "_" + FILENAME_BASE; fileNameWithoutDirectory = currentDateandTime + "_" + FILENAME_BASE; fileToStoreTo = new File(fullFileName); } } ================================================ FILE: TickTagAndroidApp/app/src/main/java/com/timm/ticktagandroidapp/StorageGeneral.java ================================================ package com.timm.ticktagandroidapp; import android.content.Context; import android.os.Environment; import android.os.StatFs; import java.io.File; public class StorageGeneral { private static final long MINIMUM_MEMORY = (16L * 1024L * 1024L); // 16 MByte public static long getMinimumMemory() { return MINIMUM_MEMORY; } public static boolean enoughFreeMemoryToStoreShit() { if(getAvailableInternalMemorySize() < MINIMUM_MEMORY) { return false; } return true; } public static long getAvailableInternalMemorySize() { File path = Environment.getDataDirectory(); StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSizeLong(); long availableBlocks = stat.getAvailableBlocksLong(); return availableBlocks * blockSize; } public static String getAvailableInternalMemorySizeAsStringInMByte() { long mByteFree = (getAvailableInternalMemorySize() / (1024 * 1024)); String mbyteWithPoint = String.format("%,d", mByteFree); return mbyteWithPoint; } public static String getStorageDirectory(Context context) { return context.getExternalFilesDir(null).getPath().toString(); } } ================================================ FILE: TickTagAndroidApp/app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: TickTagAndroidApp/app/src/main/res/drawable-v24/chart_fade.xml ================================================ ================================================ FILE: TickTagAndroidApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: TickTagAndroidApp/app/src/main/res/layout/activity_decoder.xml ================================================ ================================================ FILE: TickTagAndroidApp/app/src/main/res/layout/activity_detail.xml ================================================ ================================================ FILE: TickTagAndroidApp/app/src/main/res/layout/activity_main.xml ================================================